Compare commits

...

10 Commits

117 changed files with 4548 additions and 1489 deletions

45
Cargo.lock generated
View File

@@ -552,6 +552,7 @@ dependencies = [
"diligent-date-parser", "diligent-date-parser",
"never", "never",
"quick-xml", "quick-xml",
"serde",
] ]
[[package]] [[package]]
@@ -1579,6 +1580,15 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "croner"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c344b0690c1ad1c7176fe18eb173e0c927008fdaaa256e40dfd43ddd149c0843"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.15"
@@ -6749,6 +6759,7 @@ dependencies = [
"cocoon", "cocoon",
"color-eyre", "color-eyre",
"convert_case 0.8.0", "convert_case 0.8.0",
"croner",
"ctor", "ctor",
"dotenvy", "dotenvy",
"downloader", "downloader",
@@ -6784,6 +6795,7 @@ dependencies = [
"openidconnect", "openidconnect",
"percent-encoding", "percent-encoding",
"polars", "polars",
"quick-xml",
"quirks_path", "quirks_path",
"rand 0.9.1", "rand 0.9.1",
"regex", "regex",
@@ -6812,6 +6824,7 @@ dependencies = [
"tracing-appender", "tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"tracing-tree", "tracing-tree",
"ts-rs",
"typed-builder 0.21.0", "typed-builder 0.21.0",
"url", "url",
"util", "util",
@@ -7216,6 +7229,7 @@ dependencies = [
"derive_builder", "derive_builder",
"never", "never",
"quick-xml", "quick-xml",
"serde",
] ]
[[package]] [[package]]
@@ -8677,6 +8691,15 @@ dependencies = [
"unic-segment", "unic-segment",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "testcontainers" name = "testcontainers"
version = "0.24.0" version = "0.24.0"
@@ -9201,6 +9224,28 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
dependencies = [
"thiserror 2.0.12",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"termcolor",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.26.2" version = "0.26.2"

View File

@@ -1,17 +0,0 @@
HOST="konobangu.com"
DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu"
STORAGE_DATA_DIR = "./data"
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# OIDC_EXTRA_CLAIM_KEY = ""
# OIDC_EXTRA_CLAIM_VALUE = ""
# MIKAN_PROXY = ""
# MIKAN_PROXY_AUTH_HEADER = ""
# MIKAN_NO_PROXY = ""
# MIKAN_PROXY_ACCEPT_INVALID_CERTS = "true"

View File

@@ -1,17 +0,0 @@
HOST="konobangu.com"
DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu"
STORAGE_DATA_DIR = "./data"
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# OIDC_EXTRA_CLAIM_KEY = ""
# OIDC_EXTRA_CLAIM_VALUE = ""
MIKAN_PROXY = "http://127.0.0.1:8899"
# MIKAN_PROXY_AUTH_HEADER = ""
# MIKAN_NO_PROXY = ""
MIKAN_PROXY_ACCEPT_INVALID_CERTS = true

View File

@@ -0,0 +1,18 @@
LOGGER__LEVEL = "debug"
DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu"
AUTH__AUTH_TYPE = "basic"
AUTH__BASIC_USER = "konobangu"
AUTH__BASIC_PASSWORD = "konobangu"
# AUTH__OIDC_ISSUER = "https://auth.logto.io/oidc"
# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# AUTH__OIDC_CLIENT_ID = "client_id"
# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# AUTH__OIDC_EXTRA_CLAIM_KEY = ""
# AUTH__OIDC_EXTRA_CLAIM_VALUE = ""
MIKAN__HTTP_CLIENT__PROXY__ACCEPT_INVALID_CERTS = true
MIKAN__HTTP_CLIENT__PROXY__SERVER = "http://127.0.0.1:8899"

View File

@@ -0,0 +1,15 @@
HOST="konobangu.com"
DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu"
AUTH__AUTH_TYPE = "basic" # or oidc
AUTH__BASIC_USER = "konobangu"
AUTH__BASIC_PASSWORD = "konobangu"
# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# AUTH__OIDC_CLIENT_ID = "client_id"
# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# AUTH__OIDC_EXTRA_CLAIM_KEY = ""
# AUTH__OIDC_EXTRA_CLAIM_VALUE = ""

View File

@@ -29,3 +29,5 @@ temp/*
!temp/.gitkeep !temp/.gitkeep
tests/resources/mikan/classic_episodes/*/* tests/resources/mikan/classic_episodes/*/*
!tests/resources/mikan/classic_episodes/parquet/tiny.parquet !tests/resources/mikan/classic_episodes/parquet/tiny.parquet
webui/
data/

View File

@@ -108,7 +108,7 @@ sea-orm = { version = "1.1", features = [
] } ] }
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] } figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
sea-orm-migration = { version = "1.1", features = ["runtime-tokio"] } sea-orm-migration = { version = "1.1", features = ["runtime-tokio"] }
rss = "2" rss = { version = "2", features = ["builders", "with-serde"] }
fancy-regex = "0.14" fancy-regex = "0.14"
lightningcss = "1.0.0-alpha.66" lightningcss = "1.0.0-alpha.66"
html-escape = "0.2.13" html-escape = "0.2.13"
@@ -126,7 +126,7 @@ seaography = { version = "1.1", features = [
"with-postgres-array", "with-postgres-array",
"with-json-as-scalar", "with-json-as-scalar",
] } ] }
tower = "0.5.2" tower = { version = "0.5.2", features = ["util"] }
tower-http = { version = "0.6", features = [ tower-http = { version = "0.6", features = [
"trace", "trace",
"catch-panic", "catch-panic",
@@ -159,6 +159,13 @@ polars = { version = "0.49.1", features = [
"lazy", "lazy",
"diagonal_concat", "diagonal_concat",
], optional = true } ], optional = true }
quick-xml = { version = "0.37.5", features = [
"serialize",
"serde-types",
"serde",
] }
croner = "2.2.0"
ts-rs = "11.0.1"
[dev-dependencies] [dev-dependencies]
inquire = { workspace = true } inquire = { workspace = true }

View File

@@ -1,4 +1,4 @@
use std::time::Duration; use std::{str::FromStr, time::Duration};
use color_eyre::{Result, eyre::OptionExt}; use color_eyre::{Result, eyre::OptionExt};
use fetch::{FetchError, HttpClientConfig, fetch_bytes, fetch_html, fetch_image, reqwest}; use fetch::{FetchError, HttpClientConfig, fetch_bytes, fetch_html, fetch_image, reqwest};
@@ -6,7 +6,8 @@ use inquire::{Password, Text, validator::Validation};
use recorder::{ use recorder::{
crypto::UserPassCredential, crypto::UserPassCredential,
extract::mikan::{ extract::mikan::{
MikanClient, MikanConfig, MikanRssEpisodeItem, build_mikan_bangumi_expand_subscribed_url, MikanClient, MikanConfig, MikanRssItemMeta, MikanRssRoot,
build_mikan_bangumi_expand_subscribed_url,
extract_mikan_bangumi_index_meta_list_from_season_flow_fragment, extract_mikan_bangumi_index_meta_list_from_season_flow_fragment,
extract_mikan_bangumi_meta_from_expand_subscribed_fragment, extract_mikan_bangumi_meta_from_expand_subscribed_fragment,
}, },
@@ -190,10 +191,10 @@ async fn main() -> Result<()> {
); );
String::from_utf8(bangumi_rss_doppel_path.read()?)? String::from_utf8(bangumi_rss_doppel_path.read()?)?
}; };
let rss_items = rss::Channel::read_from(bangumi_rss_data.as_bytes())?.items; let rss_items = MikanRssRoot::from_str(&bangumi_rss_data)?.channel.items;
rss_items rss_items
.into_iter() .into_iter()
.map(MikanRssEpisodeItem::try_from) .map(MikanRssItemMeta::try_from)
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
}?; }?;
for rss_item in rss_items { for rss_item in rss_items {

View File

@@ -1,10 +1,10 @@
use std::time::Duration; use std::{str::FromStr, time::Duration};
use fetch::{FetchError, HttpClientConfig, fetch_bytes, fetch_html, fetch_image, reqwest}; use fetch::{FetchError, HttpClientConfig, fetch_bytes, fetch_html, fetch_image, reqwest};
use recorder::{ use recorder::{
errors::RecorderResult, errors::RecorderResult,
extract::mikan::{ extract::mikan::{
MikanClient, MikanConfig, MikanRssEpisodeItem, MikanClient, MikanConfig, MikanRssItemMeta, MikanRssRoot,
extract_mikan_episode_meta_from_episode_homepage_html, extract_mikan_episode_meta_from_episode_homepage_html,
}, },
test_utils::mikan::{MikanDoppelMeta, MikanDoppelPath}, test_utils::mikan::{MikanDoppelMeta, MikanDoppelPath},
@@ -41,12 +41,12 @@ async fn main() -> RecorderResult<()> {
let mikan_base_url = mikan_scrape_client.base_url().clone(); let mikan_base_url = mikan_scrape_client.base_url().clone();
tracing::info!("Scraping subscriber subscription..."); tracing::info!("Scraping subscriber subscription...");
let subscriber_subscription = let subscriber_subscription =
fs::read("tests/resources/mikan/doppel/RSS/MyBangumi-token%3Dtest.html").await?; fs::read_to_string("tests/resources/mikan/doppel/RSS/MyBangumi-token%3Dtest.html").await?;
let channel = rss::Channel::read_from(&subscriber_subscription[..])?; let channel = MikanRssRoot::from_str(&subscriber_subscription)?.channel;
let rss_items: Vec<MikanRssEpisodeItem> = channel let rss_items: Vec<MikanRssItemMeta> = channel
.items .items
.into_iter() .into_iter()
.map(MikanRssEpisodeItem::try_from) .map(MikanRssItemMeta::try_from)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
for rss_item in rss_items { for rss_item in rss_items {
let episode_homepage_meta = { let episode_homepage_meta = {
@@ -150,11 +150,11 @@ async fn main() -> RecorderResult<()> {
String::from_utf8(bangumi_rss_doppel_path.read()?)? String::from_utf8(bangumi_rss_doppel_path.read()?)?
}; };
let channel = rss::Channel::read_from(bangumi_rss_data.as_bytes())?; let rss_items: Vec<MikanRssItemMeta> = MikanRssRoot::from_str(&bangumi_rss_data)?
let rss_items: Vec<MikanRssEpisodeItem> = channel .channel
.items .items
.into_iter() .into_iter()
.map(MikanRssEpisodeItem::try_from) .map(MikanRssItemMeta::try_from)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
for rss_item in rss_items { for rss_item in rss_items {
{ {

View File

@@ -4,8 +4,8 @@
enable = true enable = true
# Enable pretty backtrace (sets RUST_BACKTRACE=1) # Enable pretty backtrace (sets RUST_BACKTRACE=1)
pretty_backtrace = true pretty_backtrace = true
level = "info"
# Log level, options: trace, debug, info, warn or error. # Log level, options: trace, debug, info, warn or error.
level = "debug"
# Define the logging format. options: compact, pretty or Json # Define the logging format. options: compact, pretty or Json
format = "compact" format = "compact"
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
@@ -77,7 +77,7 @@ max_connections = 10
auto_migrate = true auto_migrate = true
[storage] [storage]
data_dir = '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}' data_dir = './data'
[mikan] [mikan]
base_url = "https://mikanani.me/" base_url = "https://mikanani.me/"
@@ -89,26 +89,6 @@ leaky_bucket_initial_tokens = 1
leaky_bucket_refill_tokens = 1 leaky_bucket_refill_tokens = 1
leaky_bucket_refill_interval = 500 leaky_bucket_refill_interval = 500
[mikan.http_client.proxy]
server = '{{ get_env(name="MIKAN_PROXY", default = "") }}'
auth_header = '{{ get_env(name="MIKAN_PROXY_AUTH_HEADER", default = "") }}'
no_proxy = '{{ get_env(name="MIKAN_NO_PROXY", default = "") }}'
accept_invalid_certs = '{{ get_env(name="MIKAN_PROXY_ACCEPT_INVALID_CERTS", default = "false") }}'
[auth]
auth_type = '{{ get_env(name="AUTH_TYPE", default = "basic") }}'
basic_user = '{{ get_env(name="BASIC_USER", default = "konobangu") }}'
basic_password = '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}'
oidc_issuer = '{{ get_env(name="OIDC_ISSUER", default = "") }}'
oidc_audience = '{{ get_env(name="OIDC_AUDIENCE", default = "") }}'
oidc_client_id = '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}'
oidc_client_secret = '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}'
oidc_extra_scopes = '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}'
oidc_extra_claim_key = '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}'
oidc_extra_claim_value = '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}'
[graphql] [graphql]
# depth_limit = inf # depth_limit = inf
# complexity_limit = inf # complexity_limit = inf

View File

@@ -72,6 +72,11 @@ impl AppBuilder {
} }
pub async fn build(self) -> RecorderResult<App> { pub async fn build(self) -> RecorderResult<App> {
if self.working_dir != "." {
std::env::set_current_dir(&self.working_dir)?;
println!("set current dir to working dir: {}", self.working_dir);
}
self.load_env().await?; self.load_env().await?;
let config = self.load_config().await?; let config = self.load_config().await?;
@@ -86,22 +91,12 @@ impl AppBuilder {
} }
pub async fn load_env(&self) -> RecorderResult<()> { pub async fn load_env(&self) -> RecorderResult<()> {
AppConfig::load_dotenv( AppConfig::load_dotenv(&self.environment, self.dotenv_file.as_deref()).await?;
&self.environment,
&self.working_dir,
self.dotenv_file.as_deref(),
)
.await?;
Ok(()) Ok(())
} }
pub async fn load_config(&self) -> RecorderResult<AppConfig> { pub async fn load_config(&self) -> RecorderResult<AppConfig> {
let config = AppConfig::load_config( let config = AppConfig::load_config(&self.environment, self.config_file.as_deref()).await?;
&self.environment,
&self.working_dir,
self.config_file.as_deref(),
)
.await?;
Ok(config) Ok(config)
} }
@@ -136,7 +131,7 @@ impl AppBuilder {
} }
pub fn working_dir_from_manifest_dir(self) -> Self { pub fn working_dir_from_manifest_dir(self) -> Self {
let manifest_dir = if cfg!(debug_assertions) || cfg!(test) { let manifest_dir = if cfg!(debug_assertions) || cfg!(test) || cfg!(feature = "playground") {
env!("CARGO_MANIFEST_DIR") env!("CARGO_MANIFEST_DIR")
} else { } else {
"./apps/recorder" "./apps/recorder"

View File

@@ -1,8 +1,13 @@
use std::{fs, path::Path, str}; use std::{
collections::HashMap,
fs,
path::Path,
str::{self, FromStr},
};
use figment::{ use figment::{
Figment, Provider, Figment, Provider,
providers::{Format, Json, Toml, Yaml}, providers::{Env, Format, Json, Toml, Yaml},
}; };
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -55,8 +60,8 @@ impl AppConfig {
format!(".{}.local", environment.full_name()), format!(".{}.local", environment.full_name()),
format!(".{}.local", environment.short_name()), format!(".{}.local", environment.short_name()),
String::from(".local"), String::from(".local"),
environment.full_name().to_string(), format!(".{}", environment.full_name()),
environment.short_name().to_string(), format!(".{}", environment.short_name()),
String::from(""), String::from(""),
] ]
} }
@@ -65,6 +70,102 @@ impl AppConfig {
Toml::string(DEFAULT_CONFIG_MIXIN) Toml::string(DEFAULT_CONFIG_MIXIN)
} }
fn build_enhanced_tera_engine() -> tera::Tera {
let mut tera = tera::Tera::default();
tera.register_filter(
"cast_to",
|value: &tera::Value,
args: &HashMap<String, tera::Value>|
-> tera::Result<tera::Value> {
let target_type = args
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("invalid target type: should be string"))?;
let target_type = TeraCastToFilterType::from_str(target_type)
.map_err(|e| tera::Error::msg(format!("invalid target type: {e}")))?;
let input_str = value.as_str().unwrap_or("");
match target_type {
TeraCastToFilterType::Boolean => {
let is_true = matches!(input_str.to_lowercase().as_str(), "true" | "1");
let is_false = matches!(input_str.to_lowercase().as_str(), "false" | "0");
if is_true {
Ok(tera::Value::Bool(true))
} else if is_false {
Ok(tera::Value::Bool(false))
} else {
Err(tera::Error::msg(
"target type is bool but value is not a boolean like true, false, \
1, 0",
))
}
}
TeraCastToFilterType::Integer => {
let parsed = input_str.parse::<i64>().map_err(|e| {
tera::Error::call_filter("invalid integer".to_string(), e)
})?;
Ok(tera::Value::Number(serde_json::Number::from(parsed)))
}
TeraCastToFilterType::Unsigned => {
let parsed = input_str.parse::<u64>().map_err(|e| {
tera::Error::call_filter("invalid unsigned integer".to_string(), e)
})?;
Ok(tera::Value::Number(serde_json::Number::from(parsed)))
}
TeraCastToFilterType::Float => {
let parsed = input_str.parse::<f64>().map_err(|e| {
tera::Error::call_filter("invalid float".to_string(), e)
})?;
Ok(tera::Value::Number(
serde_json::Number::from_f64(parsed).ok_or_else(|| {
tera::Error::msg("failed to convert f64 to serde_json::Number")
})?,
))
}
TeraCastToFilterType::String => Ok(tera::Value::String(input_str.to_string())),
TeraCastToFilterType::Null => Ok(tera::Value::Null),
}
},
);
tera.register_filter(
"try_auto_cast",
|value: &tera::Value,
_args: &HashMap<String, tera::Value>|
-> tera::Result<tera::Value> {
let input_str = value.as_str().unwrap_or("");
if input_str == "null" {
return Ok(tera::Value::Null);
}
if matches!(input_str, "true" | "false") {
return Ok(tera::Value::Bool(input_str == "true"));
}
if let Ok(parsed) = input_str.parse::<i64>() {
return Ok(tera::Value::Number(serde_json::Number::from(parsed)));
}
if let Ok(parsed) = input_str.parse::<u64>() {
return Ok(tera::Value::Number(serde_json::Number::from(parsed)));
}
if let Ok(parsed) = input_str.parse::<f64>() {
return Ok(tera::Value::Number(
serde_json::Number::from_f64(parsed).ok_or_else(|| {
tera::Error::msg("failed to convert f64 to serde_json::Number")
})?,
));
}
Ok(tera::Value::String(input_str.to_string()))
},
);
tera
}
pub fn merge_provider_from_file( pub fn merge_provider_from_file(
fig: Figment, fig: Figment,
filepath: impl AsRef<Path>, filepath: impl AsRef<Path>,
@@ -72,11 +173,9 @@ impl AppConfig {
) -> RecorderResult<Figment> { ) -> RecorderResult<Figment> {
let content = fs::read_to_string(filepath)?; let content = fs::read_to_string(filepath)?;
let rendered = tera::Tera::one_off( let mut tera_engine = AppConfig::build_enhanced_tera_engine();
&content, let rendered =
&tera::Context::from_value(serde_json::json!({}))?, tera_engine.render_str(&content, &tera::Context::from_value(serde_json::json!({}))?)?;
false,
)?;
Ok(match ext { Ok(match ext {
".toml" => fig.merge(Toml::string(&rendered)), ".toml" => fig.merge(Toml::string(&rendered)),
@@ -88,13 +187,12 @@ impl AppConfig {
pub async fn load_dotenv( pub async fn load_dotenv(
environment: &Environment, environment: &Environment,
working_dir: &str,
dotenv_file: Option<&str>, dotenv_file: Option<&str>,
) -> RecorderResult<()> { ) -> RecorderResult<()> {
let try_dotenv_file_or_dirs = if dotenv_file.is_some() { let try_dotenv_file_or_dirs = if dotenv_file.is_some() {
vec![dotenv_file] vec![dotenv_file]
} else { } else {
vec![Some(working_dir)] vec![Some(".")]
}; };
let priority_suffix = &AppConfig::priority_suffix(environment); let priority_suffix = &AppConfig::priority_suffix(environment);
@@ -111,11 +209,16 @@ impl AppConfig {
for f in try_filenames.iter() { for f in try_filenames.iter() {
let p = try_dotenv_file_or_dir_path.join(f); let p = try_dotenv_file_or_dir_path.join(f);
if p.exists() && p.is_file() { if p.exists() && p.is_file() {
println!("Loading dotenv file: {}", p.display());
dotenvy::from_path(p)?; dotenvy::from_path(p)?;
break; break;
} }
} }
} else if try_dotenv_file_or_dir_path.is_file() { } else if try_dotenv_file_or_dir_path.is_file() {
println!(
"Loading dotenv file: {}",
try_dotenv_file_or_dir_path.display()
);
dotenvy::from_path(try_dotenv_file_or_dir_path)?; dotenvy::from_path(try_dotenv_file_or_dir_path)?;
break; break;
} }
@@ -127,13 +230,12 @@ impl AppConfig {
pub async fn load_config( pub async fn load_config(
environment: &Environment, environment: &Environment,
working_dir: &str,
config_file: Option<&str>, config_file: Option<&str>,
) -> RecorderResult<AppConfig> { ) -> RecorderResult<AppConfig> {
let try_config_file_or_dirs = if config_file.is_some() { let try_config_file_or_dirs = if config_file.is_some() {
vec![config_file] vec![config_file]
} else { } else {
vec![Some(working_dir)] vec![Some(".")]
}; };
let allowed_extensions = &AppConfig::allowed_extension(); let allowed_extensions = &AppConfig::allowed_extension();
@@ -159,6 +261,7 @@ impl AppConfig {
let p = try_config_file_or_dir_path.join(f); let p = try_config_file_or_dir_path.join(f);
if p.exists() && p.is_file() { if p.exists() && p.is_file() {
fig = AppConfig::merge_provider_from_file(fig, &p, ext)?; fig = AppConfig::merge_provider_from_file(fig, &p, ext)?;
println!("Loaded config file: {}", p.display());
break; break;
} }
} }
@@ -169,13 +272,52 @@ impl AppConfig {
{ {
fig = fig =
AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?; AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?;
println!(
"Loaded config file: {}",
try_config_file_or_dir_path.display()
);
break; break;
} }
} }
} }
fig = fig.merge(Env::prefixed("").split("__").lowercase(true));
let app_config: AppConfig = fig.extract()?; let app_config: AppConfig = fig.extract()?;
Ok(app_config) Ok(app_config)
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum TeraCastToFilterType {
#[serde(alias = "str")]
String,
#[serde(alias = "bool")]
Boolean,
#[serde(alias = "int")]
Integer,
#[serde(alias = "uint")]
Unsigned,
#[serde(alias = "float")]
Float,
#[serde(alias = "null")]
Null,
}
impl FromStr for TeraCastToFilterType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"string" | "str" => Ok(TeraCastToFilterType::String),
"boolean" | "bool" => Ok(TeraCastToFilterType::Boolean),
"integer" | "int" => Ok(TeraCastToFilterType::Integer),
"unsigned" | "uint" => Ok(TeraCastToFilterType::Unsigned),
"float" => Ok(TeraCastToFilterType::Float),
"null" => Ok(TeraCastToFilterType::Null),
_ => Err(format!("invalid target type: {s}")),
}
}
}

View File

@@ -1,11 +1,13 @@
use std::{net::SocketAddr, sync::Arc}; use std::{net::SocketAddr, sync::Arc};
use axum::Router; use axum::{Router, middleware::from_fn_with_state};
use tokio::{net::TcpSocket, signal}; use tokio::{net::TcpSocket, signal};
use tower_http::services::{ServeDir, ServeFile};
use tracing::instrument; use tracing::instrument;
use super::{builder::AppBuilder, context::AppContextTrait}; use super::{builder::AppBuilder, context::AppContextTrait};
use crate::{ use crate::{
auth::webui_auth_middleware,
errors::{RecorderError, RecorderResult}, errors::{RecorderError, RecorderResult},
web::{ web::{
controller::{self, core::ControllerTrait}, controller::{self, core::ControllerTrait},
@@ -58,13 +60,19 @@ impl App {
controller::oidc::create(context.clone()), controller::oidc::create(context.clone()),
controller::metadata::create(context.clone()), controller::metadata::create(context.clone()),
controller::r#static::create(context.clone()), controller::r#static::create(context.clone()),
controller::feeds::create(context.clone()), controller::feeds::create(context.clone())
)?; )?;
for c in [graphql_c, oidc_c, metadata_c, static_c, feeds_c] { for c in [graphql_c, oidc_c, metadata_c, static_c, feeds_c] {
router = c.apply_to(router); router = c.apply_to(router);
} }
router = router
.fallback_service(
ServeDir::new("webui").not_found_service(ServeFile::new("webui/index.html")),
)
.layer(from_fn_with_state(context.clone(), webui_auth_middleware));
let middlewares = default_middleware_stack(context.clone()); let middlewares = default_middleware_stack(context.clone());
for mid in middlewares { for mid in middlewares {
if mid.is_enabled() { if mid.is_enabled() {
@@ -99,26 +107,12 @@ impl App {
Ok::<(), RecorderError>(()) Ok::<(), RecorderError>(())
}, },
async { async {
{ task.run(if graceful_shutdown {
let monitor = task.setup_monitor().await?; Some(Self::shutdown_signal)
if graceful_shutdown { } else {
monitor None
.run_with_signal(async move {
Self::shutdown_signal().await;
tracing::info!("apalis shutting down...");
Ok(())
}) })
.await?; .await?;
} else {
monitor.run().await?;
}
}
Ok::<(), RecorderError>(())
},
async {
let listener = task.setup_listener().await?;
listener.listen().await?;
Ok::<(), RecorderError>(()) Ok::<(), RecorderError>(())
} }

View File

@@ -7,7 +7,10 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use crate::{app::AppContextTrait, auth::AuthServiceTrait}; use crate::{
app::AppContextTrait,
auth::{AuthService, AuthServiceTrait},
};
pub async fn auth_middleware( pub async fn auth_middleware(
State(ctx): State<Arc<dyn AppContextTrait>>, State(ctx): State<Arc<dyn AppContextTrait>>,
@@ -38,3 +41,37 @@ pub async fn auth_middleware(
response response
} }
pub async fn webui_auth_middleware(
State(ctx): State<Arc<dyn AppContextTrait>>,
request: Request,
next: Next,
) -> Response {
if (!request.uri().path().starts_with("/api"))
&& let AuthService::Basic(auth_service) = ctx.auth()
{
let (mut parts, body) = request.into_parts();
let mut response = match auth_service
.extract_user_info(ctx.as_ref() as &dyn AppContextTrait, &mut parts)
.await
{
Ok(auth_user_info) => {
let mut request = Request::from_parts(parts, body);
request.extensions_mut().insert(auth_user_info);
next.run(request).await
}
Err(auth_error) => auth_error.into_response(),
};
if let Some(header_value) = auth_service.www_authenticate_header_value() {
response
.headers_mut()
.insert(header::WWW_AUTHENTICATE, header_value);
};
response
} else {
next.run(request).await
}
}

View File

@@ -7,5 +7,5 @@ pub mod service;
pub use config::{AuthConfig, BasicAuthConfig, OidcAuthConfig}; pub use config::{AuthConfig, BasicAuthConfig, OidcAuthConfig};
pub use errors::AuthError; pub use errors::AuthError;
pub use middleware::auth_middleware; pub use middleware::{auth_middleware, webui_auth_middleware};
pub use service::{AuthService, AuthServiceTrait, AuthUserInfo}; pub use service::{AuthService, AuthServiceTrait, AuthUserInfo};

View File

@@ -21,7 +21,6 @@ use openidconnect::{
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse,
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}, core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
}; };
use sea_orm::DbErr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use snafu::ResultExt; use snafu::ResultExt;
@@ -338,9 +337,9 @@ impl AuthServiceTrait for OidcAuthService {
} }
} }
let subscriber_auth = match crate::models::auth::Model::find_by_pid(ctx, sub).await { let subscriber_auth = match crate::models::auth::Model::find_by_pid(ctx, sub).await {
Err(RecorderError::DbError { Err(RecorderError::ModelEntityNotFound { .. }) => {
source: DbErr::RecordNotFound(..), crate::models::auth::Model::create_from_oidc(ctx, sub.to_string()).await
}) => crate::models::auth::Model::create_from_oidc(ctx, sub.to_string()).await, }
r => r, r => r,
} }
.map_err(|e| { .map_err(|e| {

View File

@@ -18,6 +18,10 @@ use crate::{
#[derive(Snafu, Debug)] #[derive(Snafu, Debug)]
#[snafu(visibility(pub(crate)))] #[snafu(visibility(pub(crate)))]
pub enum RecorderError { pub enum RecorderError {
#[snafu(transparent)]
SeaographyError { source: seaography::SeaographyError },
#[snafu(transparent)]
CronError { source: croner::errors::CronError },
#[snafu(display( #[snafu(display(
"HTTP {status} {reason}, source = {source:?}", "HTTP {status} {reason}, source = {source:?}",
status = status, status = status,
@@ -49,6 +53,8 @@ pub enum RecorderError {
InvalidMethodError, InvalidMethodError,
#[snafu(display("Invalid header value"))] #[snafu(display("Invalid header value"))]
InvalidHeaderValueError, InvalidHeaderValueError,
#[snafu(transparent)]
QuickXmlDeserializeError { source: quick_xml::DeError },
#[snafu(display("Invalid header name"))] #[snafu(display("Invalid header name"))]
InvalidHeaderNameError, InvalidHeaderNameError,
#[snafu(display("Missing origin (protocol or host) in headers and forwarded info"))] #[snafu(display("Missing origin (protocol or host) in headers and forwarded info"))]
@@ -118,8 +124,13 @@ pub enum RecorderError {
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))] #[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))]
source: OptDynErr, source: OptDynErr,
}, },
#[snafu(display("Model Entity {entity} not found or not belong to subscriber"))] #[snafu(display("Model Entity {entity} not found or not belong to subscriber{}", (
ModelEntityNotFound { entity: Cow<'static, str> }, detail.as_ref().map(|detail| format!(" : {detail}"))).unwrap_or_default()
))]
ModelEntityNotFound {
entity: Cow<'static, str>,
detail: Option<String>,
},
#[snafu(transparent)] #[snafu(transparent)]
FetchError { source: FetchError }, FetchError { source: FetchError },
#[snafu(display("Credential3rdError: {message}, source = {source}"))] #[snafu(display("Credential3rdError: {message}, source = {source}"))]
@@ -183,9 +194,17 @@ impl RecorderError {
} }
} }
pub fn from_db_record_not_found<T: ToString>(detail: T) -> Self { pub fn from_entity_not_found<E: sea_orm::EntityTrait>() -> Self {
Self::DbError { Self::ModelEntityNotFound {
source: sea_orm::DbErr::RecordNotFound(detail.to_string()), entity: std::any::type_name::<E::Model>().into(),
detail: None,
}
}
pub fn from_entity_not_found_detail<E: sea_orm::EntityTrait, T: ToString>(detail: T) -> Self {
Self::ModelEntityNotFound {
entity: std::any::type_name::<E::Model>().into(),
detail: Some(detail.to_string()),
} }
} }
} }
@@ -250,9 +269,9 @@ impl IntoResponse for RecorderError {
) )
.into_response() .into_response()
} }
Self::ModelEntityNotFound { entity } => ( merr @ Self::ModelEntityNotFound { .. } => (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
Json::<StandardErrorResponse>(StandardErrorResponse::from(entity.to_string())), Json::<StandardErrorResponse>(StandardErrorResponse::from(merr.to_string())),
) )
.into_response(), .into_response(),
err => ( err => (

View File

@@ -167,6 +167,7 @@ impl ForwardedRelatedInfo {
.as_ref() .as_ref()
.and_then(|s| s.host.as_deref()) .and_then(|s| s.host.as_deref())
.or(self.x_forwarded_host.as_deref()) .or(self.x_forwarded_host.as_deref())
.or(self.host.as_deref())
.or(self.uri.host()) .or(self.uri.host())
} }

View File

@@ -4,7 +4,7 @@ use fetch::{HttpClient, HttpClientTrait};
use maplit::hashmap; use maplit::hashmap;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use sea_orm::{ use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DbErr, EntityTrait, QueryFilter, TryIntoModel, ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel,
}; };
use url::Url; use url::Url;
use util::OptDynErr; use util::OptDynErr;
@@ -227,9 +227,12 @@ impl MikanClient {
self.fork_with_userpass_credential(userpass_credential) self.fork_with_userpass_credential(userpass_credential)
.await .await
} else { } else {
Err(RecorderError::from_db_record_not_found( Err(RecorderError::from_entity_not_found_detail::<
DbErr::RecordNotFound(format!("credential={credential_id} not found")), credential_3rd::Entity,
)) _,
>(format!(
"credential id {credential_id} not found"
)))
} }
} }

View File

@@ -2,6 +2,7 @@ mod client;
mod config; mod config;
mod constants; mod constants;
mod credential; mod credential;
mod rss;
mod subscription; mod subscription;
mod web; mod web;
@@ -18,17 +19,19 @@ pub use constants::{
MIKAN_UNKNOWN_FANSUB_NAME, MIKAN_YEAR_QUERY_KEY, MIKAN_UNKNOWN_FANSUB_NAME, MIKAN_YEAR_QUERY_KEY,
}; };
pub use credential::MikanCredentialForm; pub use credential::MikanCredentialForm;
pub use rss::{
MikanRssChannel, MikanRssItem, MikanRssItemMeta, MikanRssItemTorrentExtension, MikanRssRoot,
build_mikan_bangumi_subscription_rss_url, build_mikan_subscriber_subscription_rss_url,
};
pub use subscription::{ pub use subscription::{
MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription, MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription,
}; };
pub use web::{ pub use web::{
MikanBangumiHash, MikanBangumiIndexHash, MikanBangumiIndexMeta, MikanBangumiMeta, MikanBangumiHash, MikanBangumiIndexHash, MikanBangumiIndexMeta, MikanBangumiMeta,
MikanBangumiPosterMeta, MikanEpisodeHash, MikanEpisodeMeta, MikanFansubHash, MikanBangumiPosterMeta, MikanEpisodeHash, MikanEpisodeMeta, MikanFansubHash,
MikanRssEpisodeItem, MikanSeasonFlowUrlMeta, MikanSeasonStr, MikanSeasonFlowUrlMeta, MikanSeasonStr, MikanSubscriberSubscriptionUrlMeta,
MikanSubscriberSubscriptionRssUrlMeta, build_mikan_bangumi_expand_subscribed_url, build_mikan_bangumi_expand_subscribed_url, build_mikan_bangumi_homepage_url,
build_mikan_bangumi_homepage_url, build_mikan_bangumi_subscription_rss_url,
build_mikan_episode_homepage_url, build_mikan_season_flow_url, build_mikan_episode_homepage_url, build_mikan_season_flow_url,
build_mikan_subscriber_subscription_rss_url,
extract_mikan_bangumi_index_meta_list_from_season_flow_fragment, extract_mikan_bangumi_index_meta_list_from_season_flow_fragment,
extract_mikan_bangumi_meta_from_expand_subscribed_fragment, extract_mikan_bangumi_meta_from_expand_subscribed_fragment,
extract_mikan_episode_meta_from_episode_homepage_html, extract_mikan_episode_meta_from_episode_homepage_html,

View File

@@ -0,0 +1,215 @@
use std::{borrow::Cow, str::FromStr};
use chrono::{DateTime, Utc};
use downloader::bittorrent::defs::BITTORRENT_MIME_TYPE;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
errors::{RecorderResult, app_error::RecorderError},
extract::{
bittorrent::EpisodeEnclosureMeta,
mikan::{
MIKAN_BANGUMI_ID_QUERY_KEY, MIKAN_BANGUMI_RSS_PATH, MIKAN_FANSUB_ID_QUERY_KEY,
MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
MikanEpisodeHash, build_mikan_episode_homepage_url,
},
},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MikanRssItemEnclosure {
#[serde(rename = "@type")]
pub r#type: String,
#[serde(rename = "@length")]
pub length: i64,
#[serde(rename = "@url")]
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MikanRssItemTorrentExtension {
pub pub_date: String,
pub content_length: i64,
pub link: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MikanRssItem {
pub torrent: MikanRssItemTorrentExtension,
pub link: String,
pub title: String,
pub enclosure: MikanRssItemEnclosure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MikanRssChannel {
#[serde(rename = "item", default)]
pub items: Vec<MikanRssItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MikanRssRoot {
pub channel: MikanRssChannel,
}
impl FromStr for MikanRssRoot {
type Err = RecorderError;
fn from_str(source: &str) -> RecorderResult<Self> {
let me = quick_xml::de::from_str(source)?;
Ok(me)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MikanRssItemMeta {
pub title: String,
pub torrent_link: Url,
pub content_length: i64,
pub mime: String,
pub pub_date: Option<DateTime<Utc>>,
pub mikan_episode_id: String,
pub magnet_link: Option<String>,
}
impl MikanRssItemMeta {
pub fn build_homepage_url(&self, mikan_base_url: Url) -> Url {
build_mikan_episode_homepage_url(mikan_base_url, &self.mikan_episode_id)
}
pub fn parse_pub_date(pub_date: &str) -> chrono::ParseResult<DateTime<Utc>> {
DateTime::parse_from_rfc2822(pub_date)
.or_else(|_| DateTime::parse_from_rfc3339(pub_date))
.or_else(|_| DateTime::parse_from_rfc3339(&format!("{pub_date}+08:00")))
.map(|s| s.with_timezone(&Utc))
}
}
impl TryFrom<MikanRssItem> for MikanRssItemMeta {
type Error = RecorderError;
fn try_from(item: MikanRssItem) -> Result<Self, Self::Error> {
let torrent = item.torrent;
let enclosure = item.enclosure;
let mime_type = enclosure.r#type;
if mime_type != BITTORRENT_MIME_TYPE {
return Err(RecorderError::MimeError {
expected: String::from(BITTORRENT_MIME_TYPE),
found: mime_type.to_string(),
desc: String::from("MikanRssItem"),
});
}
let title = item.title;
let enclosure_url = Url::parse(&enclosure.url).map_err(|err| {
RecorderError::from_mikan_rss_invalid_field_and_source(
"enclosure_url:enclosure.link".into(),
err,
)
})?;
let homepage = Url::parse(&item.link).map_err(|err| {
RecorderError::from_mikan_rss_invalid_field_and_source(
"enclosure_url:enclosure.link".into(),
err,
)
})?;
let MikanEpisodeHash {
mikan_episode_id, ..
} = MikanEpisodeHash::from_homepage_url(&homepage).ok_or_else(|| {
RecorderError::from_mikan_rss_invalid_field(Cow::Borrowed("mikan_episode_id"))
})?;
Ok(MikanRssItemMeta {
title,
torrent_link: enclosure_url,
content_length: enclosure.length,
mime: mime_type,
pub_date: Self::parse_pub_date(&torrent.pub_date).ok(),
mikan_episode_id,
magnet_link: None,
})
}
}
impl From<MikanRssItemMeta> for EpisodeEnclosureMeta {
fn from(item: MikanRssItemMeta) -> Self {
Self {
magnet_link: item.magnet_link,
torrent_link: Some(item.torrent_link.to_string()),
pub_date: item.pub_date,
content_length: Some(item.content_length),
}
}
}
pub fn build_mikan_subscriber_subscription_rss_url(
mikan_base_url: Url,
mikan_subscription_token: &str,
) -> Url {
let mut url = mikan_base_url;
url.set_path(MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH);
url.query_pairs_mut().append_pair(
MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
mikan_subscription_token,
);
url
}
pub fn build_mikan_bangumi_subscription_rss_url(
mikan_base_url: Url,
mikan_bangumi_id: &str,
mikan_fansub_id: Option<&str>,
) -> Url {
let mut url = mikan_base_url;
url.set_path(MIKAN_BANGUMI_RSS_PATH);
url.query_pairs_mut()
.append_pair(MIKAN_BANGUMI_ID_QUERY_KEY, mikan_bangumi_id);
if let Some(mikan_fansub_id) = mikan_fansub_id {
url.query_pairs_mut()
.append_pair(MIKAN_FANSUB_ID_QUERY_KEY, mikan_fansub_id);
};
url
}
#[cfg(test)]
mod test {
#![allow(unused_variables)]
use std::fs;
use rstest::{fixture, rstest};
use tracing::Level;
use super::*;
use crate::{errors::RecorderResult, test_utils::tracing::try_init_testing_tracing};
#[fixture]
fn before_each() {
try_init_testing_tracing(Level::DEBUG);
}
#[rstest]
#[test]
fn test_mikan_rss_episode_item_try_from_rss_item(before_each: ()) -> RecorderResult<()> {
let rss_str = fs::read_to_string(
"tests/resources/mikan/doppel/RSS/Bangumi-bangumiId%3D3288%26subgroupid%3D370.html",
)?;
let mut channel = MikanRssRoot::from_str(&rss_str)?.channel;
assert!(!channel.items.is_empty());
let item = channel.items.pop().unwrap();
let episode_item = MikanRssItemMeta::try_from(item.clone())?;
assert!(episode_item.pub_date.is_some());
Ok(())
}
}

View File

@@ -1,12 +1,13 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt::Debug, fmt::Debug,
str::FromStr,
sync::Arc, sync::Arc,
}; };
use async_graphql::{InputObject, SimpleObject}; use async_graphql::{InputObject, SimpleObject};
use async_stream::try_stream; use async_stream::try_stream;
use fetch::fetch_bytes; use fetch::fetch_html;
use futures::{Stream, TryStreamExt, pin_mut, try_join}; use futures::{Stream, TryStreamExt, pin_mut, try_join};
use maplit::hashmap; use maplit::hashmap;
use sea_orm::{ use sea_orm::{
@@ -24,8 +25,8 @@ use crate::{
bittorrent::EpisodeEnclosureMeta, bittorrent::EpisodeEnclosureMeta,
mikan::{ mikan::{
MikanBangumiHash, MikanBangumiMeta, MikanEpisodeHash, MikanEpisodeMeta, MikanBangumiHash, MikanBangumiMeta, MikanEpisodeHash, MikanEpisodeMeta,
MikanRssEpisodeItem, MikanSeasonFlowUrlMeta, MikanSeasonStr, MikanRssItemMeta, MikanRssRoot, MikanSeasonFlowUrlMeta, MikanSeasonStr,
MikanSubscriberSubscriptionRssUrlMeta, build_mikan_bangumi_subscription_rss_url, MikanSubscriberSubscriptionUrlMeta, build_mikan_bangumi_subscription_rss_url,
build_mikan_season_flow_url, build_mikan_subscriber_subscription_rss_url, build_mikan_season_flow_url, build_mikan_subscriber_subscription_rss_url,
scrape_mikan_episode_meta_from_episode_homepage_url, scrape_mikan_episode_meta_from_episode_homepage_url,
}, },
@@ -39,7 +40,7 @@ use crate::{
#[tracing::instrument(err, skip(ctx, rss_item_list))] #[tracing::instrument(err, skip(ctx, rss_item_list))]
async fn sync_mikan_feeds_from_rss_item_list( async fn sync_mikan_feeds_from_rss_item_list(
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
rss_item_list: Vec<MikanRssEpisodeItem>, rss_item_list: Vec<MikanRssItemMeta>,
subscriber_id: i32, subscriber_id: i32,
subscription_id: i32, subscription_id: i32,
) -> RecorderResult<()> { ) -> RecorderResult<()> {
@@ -202,7 +203,7 @@ impl SubscriptionTrait for MikanSubscriberSubscription {
fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self> { fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self> {
let source_url = Url::parse(&model.source_url)?; let source_url = Url::parse(&model.source_url)?;
let meta = MikanSubscriberSubscriptionRssUrlMeta::from_rss_url(&source_url) let meta = MikanSubscriberSubscriptionUrlMeta::from_rss_url(&source_url)
.with_whatever_context::<_, String, RecorderError>(|| { .with_whatever_context::<_, String, RecorderError>(|| {
format!( format!(
"MikanSubscriberSubscription should extract mikan_subscription_token from \ "MikanSubscriberSubscription should extract mikan_subscription_token from \
@@ -224,19 +225,19 @@ impl MikanSubscriberSubscription {
async fn get_rss_item_list_from_source_url( async fn get_rss_item_list_from_source_url(
&self, &self,
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
) -> RecorderResult<Vec<MikanRssEpisodeItem>> { ) -> RecorderResult<Vec<MikanRssItemMeta>> {
let mikan_base_url = ctx.mikan().base_url().clone(); let mikan_base_url = ctx.mikan().base_url().clone();
let rss_url = build_mikan_subscriber_subscription_rss_url( let rss_url = build_mikan_subscriber_subscription_rss_url(
mikan_base_url.clone(), mikan_base_url.clone(),
&self.mikan_subscription_token, &self.mikan_subscription_token,
); );
let bytes = fetch_bytes(ctx.mikan(), rss_url).await?; let html = fetch_html(ctx.mikan(), rss_url).await?;
let channel = rss::Channel::read_from(&bytes[..])?; let channel = MikanRssRoot::from_str(&html)?.channel;
let mut result = vec![]; let mut result = vec![];
for (idx, item) in channel.items.into_iter().enumerate() { for (idx, item) in channel.items.into_iter().enumerate() {
let item = MikanRssEpisodeItem::try_from(item) let item = MikanRssItemMeta::try_from(item)
.with_whatever_context::<_, String, RecorderError>(|_| { .with_whatever_context::<_, String, RecorderError>(|_| {
format!("failed to extract rss item at idx {idx}") format!("failed to extract rss item at idx {idx}")
})?; })?;
@@ -249,7 +250,7 @@ impl MikanSubscriberSubscription {
async fn get_rss_item_list_from_subsribed_url_rss_link( async fn get_rss_item_list_from_subsribed_url_rss_link(
&self, &self,
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
) -> RecorderResult<Vec<MikanRssEpisodeItem>> { ) -> RecorderResult<Vec<MikanRssItemMeta>> {
let subscribed_bangumi_list = let subscribed_bangumi_list =
bangumi::Model::get_subsribed_bangumi_list_from_subscription(ctx, self.subscription_id) bangumi::Model::get_subsribed_bangumi_list_from_subscription(ctx, self.subscription_id)
.await?; .await?;
@@ -264,12 +265,12 @@ impl MikanSubscriberSubscription {
self.subscription_id, subscribed_bangumi.display_name self.subscription_id, subscribed_bangumi.display_name
) )
})?; })?;
let bytes = fetch_bytes(ctx.mikan(), rss_url).await?; let html = fetch_html(ctx.mikan(), rss_url).await?;
let channel = rss::Channel::read_from(&bytes[..])?; let channel = MikanRssRoot::from_str(&html)?.channel;
for (idx, item) in channel.items.into_iter().enumerate() { for (idx, item) in channel.items.into_iter().enumerate() {
let item = MikanRssEpisodeItem::try_from(item) let item = MikanRssItemMeta::try_from(item)
.with_whatever_context::<_, String, RecorderError>(|_| { .with_whatever_context::<_, String, RecorderError>(|_| {
format!("failed to extract rss item at idx {idx}") format!("failed to extract rss item at idx {idx}")
})?; })?;
@@ -406,7 +407,7 @@ impl MikanSeasonSubscription {
fn get_rss_item_stream_from_subsribed_url_rss_link( fn get_rss_item_stream_from_subsribed_url_rss_link(
&self, &self,
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
) -> impl Stream<Item = RecorderResult<Vec<MikanRssEpisodeItem>>> { ) -> impl Stream<Item = RecorderResult<Vec<MikanRssItemMeta>>> {
try_stream! { try_stream! {
let db = ctx.db(); let db = ctx.db();
@@ -433,14 +434,14 @@ impl MikanSeasonSubscription {
self.subscription_id, subscribed_bangumi.display_name self.subscription_id, subscribed_bangumi.display_name
) )
})?; })?;
let bytes = fetch_bytes(ctx.mikan(), rss_url).await?; let html = fetch_html(ctx.mikan(), rss_url).await?;
let channel = rss::Channel::read_from(&bytes[..])?; let channel = MikanRssRoot::from_str(&html)?.channel;
let mut rss_item_list = vec![]; let mut rss_item_list = vec![];
for (idx, item) in channel.items.into_iter().enumerate() { for (idx, item) in channel.items.into_iter().enumerate() {
let item = MikanRssEpisodeItem::try_from(item) let item = MikanRssItemMeta::try_from(item)
.with_whatever_context::<_, String, RecorderError>(|_| { .with_whatever_context::<_, String, RecorderError>(|_| {
format!("failed to extract rss item at idx {idx}") format!("failed to extract rss item at idx {idx}")
})?; })?;
@@ -519,20 +520,20 @@ impl MikanBangumiSubscription {
async fn get_rss_item_list_from_source_url( async fn get_rss_item_list_from_source_url(
&self, &self,
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
) -> RecorderResult<Vec<MikanRssEpisodeItem>> { ) -> RecorderResult<Vec<MikanRssItemMeta>> {
let mikan_base_url = ctx.mikan().base_url().clone(); let mikan_base_url = ctx.mikan().base_url().clone();
let rss_url = build_mikan_bangumi_subscription_rss_url( let rss_url = build_mikan_bangumi_subscription_rss_url(
mikan_base_url.clone(), mikan_base_url.clone(),
&self.mikan_bangumi_id, &self.mikan_bangumi_id,
Some(&self.mikan_fansub_id), Some(&self.mikan_fansub_id),
); );
let bytes = fetch_bytes(ctx.mikan(), rss_url).await?; let html = fetch_html(ctx.mikan(), rss_url).await?;
let channel = rss::Channel::read_from(&bytes[..])?; let channel = MikanRssRoot::from_str(&html)?.channel;
let mut result = vec![]; let mut result = vec![];
for (idx, item) in channel.items.into_iter().enumerate() { for (idx, item) in channel.items.into_iter().enumerate() {
let item = MikanRssEpisodeItem::try_from(item) let item = MikanRssItemMeta::try_from(item)
.with_whatever_context::<_, String, RecorderError>(|_| { .with_whatever_context::<_, String, RecorderError>(|_| {
format!("failed to extract rss item at idx {idx}") format!("failed to extract rss item at idx {idx}")
})?; })?;
@@ -556,7 +557,7 @@ mod tests {
errors::RecorderResult, errors::RecorderResult,
extract::mikan::{ extract::mikan::{
MikanBangumiHash, MikanSeasonFlowUrlMeta, MikanSeasonStr, MikanBangumiHash, MikanSeasonFlowUrlMeta, MikanSeasonStr,
MikanSubscriberSubscriptionRssUrlMeta, MikanSubscriberSubscriptionUrlMeta,
}, },
models::{ models::{
bangumi, episodes, bangumi, episodes,
@@ -677,7 +678,7 @@ mod tests {
subscriber_id: ActiveValue::Set(subscriber_id), subscriber_id: ActiveValue::Set(subscriber_id),
category: ActiveValue::Set(subscriptions::SubscriptionCategory::MikanSubscriber), category: ActiveValue::Set(subscriptions::SubscriptionCategory::MikanSubscriber),
source_url: ActiveValue::Set( source_url: ActiveValue::Set(
MikanSubscriberSubscriptionRssUrlMeta { MikanSubscriberSubscriptionUrlMeta {
mikan_subscription_token: "test".into(), mikan_subscription_token: "test".into(),
} }
.build_rss_url(mikan_server.base_url().clone()) .build_rss_url(mikan_server.base_url().clone())

View File

@@ -26,7 +26,8 @@ use crate::{
MIKAN_EPISODE_HOMEPAGE_PATH, MIKAN_FANSUB_HOMEPAGE_PATH, MIKAN_FANSUB_ID_QUERY_KEY, MIKAN_EPISODE_HOMEPAGE_PATH, MIKAN_FANSUB_HOMEPAGE_PATH, MIKAN_FANSUB_ID_QUERY_KEY,
MIKAN_POSTER_BUCKET_KEY, MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SEASON_STR_QUERY_KEY, MIKAN_POSTER_BUCKET_KEY, MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SEASON_STR_QUERY_KEY,
MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY, MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
MIKAN_YEAR_QUERY_KEY, MikanClient, MIKAN_YEAR_QUERY_KEY, MikanClient, build_mikan_bangumi_subscription_rss_url,
build_mikan_subscriber_subscription_rss_url,
}, },
}, },
media::{ media::{
@@ -139,16 +140,16 @@ impl From<MikanRssEpisodeItem> for EpisodeEnclosureMeta {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MikanSubscriberSubscriptionRssUrlMeta { pub struct MikanSubscriberSubscriptionUrlMeta {
pub mikan_subscription_token: String, pub mikan_subscription_token: String,
} }
impl MikanSubscriberSubscriptionRssUrlMeta { impl MikanSubscriberSubscriptionUrlMeta {
pub fn from_rss_url(url: &Url) -> Option<Self> { pub fn from_rss_url(url: &Url) -> Option<Self> {
if url.path() == MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH { if url.path() == MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH {
url.query_pairs() url.query_pairs()
.find(|(k, _)| k == MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY) .find(|(k, _)| k == MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY)
.map(|(_, v)| MikanSubscriberSubscriptionRssUrlMeta { .map(|(_, v)| MikanSubscriberSubscriptionUrlMeta {
mikan_subscription_token: v.to_string(), mikan_subscription_token: v.to_string(),
}) })
} else { } else {
@@ -161,19 +162,6 @@ impl MikanSubscriberSubscriptionRssUrlMeta {
} }
} }
pub fn build_mikan_subscriber_subscription_rss_url(
mikan_base_url: Url,
mikan_subscription_token: &str,
) -> Url {
let mut url = mikan_base_url;
url.set_path(MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH);
url.query_pairs_mut().append_pair(
MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
mikan_subscription_token,
);
url
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Eq)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Eq)]
pub struct MikanBangumiIndexMeta { pub struct MikanBangumiIndexMeta {
pub homepage: Url, pub homepage: Url,
@@ -289,22 +277,6 @@ pub struct MikanBangumiPosterMeta {
pub poster_src: Option<String>, pub poster_src: Option<String>,
} }
pub fn build_mikan_bangumi_subscription_rss_url(
mikan_base_url: Url,
mikan_bangumi_id: &str,
mikan_fansub_id: Option<&str>,
) -> Url {
let mut url = mikan_base_url;
url.set_path(MIKAN_BANGUMI_RSS_PATH);
url.query_pairs_mut()
.append_pair(MIKAN_BANGUMI_ID_QUERY_KEY, mikan_bangumi_id);
if let Some(mikan_fansub_id) = mikan_fansub_id {
url.query_pairs_mut()
.append_pair(MIKAN_FANSUB_ID_QUERY_KEY, mikan_fansub_id);
};
url
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct MikanBangumiIndexHash { pub struct MikanBangumiIndexHash {
pub mikan_bangumi_id: String, pub mikan_bangumi_id: String,
@@ -829,11 +801,6 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
.write(storage_path.clone(), poster_data) .write(storage_path.clone(), poster_data)
.await?; .await?;
tracing::warn!(
poster_str = poster_str.to_string(),
"mikan poster meta extracted"
);
MikanBangumiPosterMeta { MikanBangumiPosterMeta {
origin_poster_src: origin_poster_src_url, origin_poster_src: origin_poster_src_url,
poster_src: Some(poster_str.to_string()), poster_src: Some(poster_str.to_string()),

View File

@@ -47,8 +47,27 @@ impl<'a> EpisodeComp<'a> {
Ok((input, f32::round(num) as i32)) Ok((input, f32::round(num) as i32))
} }
fn parse_ep_special_num(input: &'a str) -> IResult<&'a str, i32> {
terminated(
alt((
value(0, tag_no_case("ova")),
value(0, tag_no_case("oad")),
value(0, tag_no_case("sp")),
value(0, tag_no_case("ex")),
)),
(space0, opt(parse_int::<i32>)),
)
.parse(input)
}
fn parse_ep_num(input: &'a str) -> IResult<&'a str, i32> { fn parse_ep_num(input: &'a str) -> IResult<&'a str, i32> {
alt((parse_int::<i32>, Self::parse_ep_round_num, ZhNum::parse_int)).parse(input) alt((
parse_int::<i32>,
Self::parse_ep_round_num,
ZhNum::parse_int,
Self::parse_ep_special_num,
))
.parse(input)
} }
fn parse_ep_nums_core(input: &'a str) -> IResult<&'a str, (i32, Option<i32>)> { fn parse_ep_nums_core(input: &'a str) -> IResult<&'a str, (i32, Option<i32>)> {
@@ -175,8 +194,13 @@ impl<'a> std::fmt::Debug for MoiveComp<'a> {
impl<'a> OriginCompTrait<'a> for MoiveComp<'a> { impl<'a> OriginCompTrait<'a> for MoiveComp<'a> {
#[cfg_attr(debug_assertions, instrument(level = Level::TRACE, ret, err(level=Level::TRACE), "MoiveComp::parse_comp"))] #[cfg_attr(debug_assertions, instrument(level = Level::TRACE, ret, err(level=Level::TRACE), "MoiveComp::parse_comp"))]
fn parse_comp(input: &'a str) -> IResult<&'a str, Self> { fn parse_comp(input: &'a str) -> IResult<&'a str, Self> {
let (input, source) = let (input, source) = alt((
alt((tag("剧场版"), tag("电影"), tag_no_case("movie"))).parse(input)?; tag("剧场版"),
tag("电影"),
tag_no_case("movie"),
tag_no_case("film"),
))
.parse(input)?;
Ok(( Ok((
input, input,
Self { Self {

View File

@@ -1,6 +1,12 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::bangumi}; use crate::{
graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::bangumi,
};
pub fn register_bangumi_to_schema_context(context: &mut BuilderContext) { pub fn register_bangumi_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<bangumi::Entity>(context, &bangumi::Column::SubscriberId); restrict_subscriber_for_entity::<bangumi::Entity>(context, &bangumi::Column::SubscriberId);
@@ -8,7 +14,6 @@ pub fn register_bangumi_to_schema_context(context: &mut BuilderContext) {
pub fn register_bangumi_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { pub fn register_bangumi_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<bangumi::BangumiType>(); builder.register_enumeration::<bangumi::BangumiType>();
seaography::register_entity!(builder, bangumi);
builder register_entity_default_writable!(builder, bangumi, false)
} }

View File

@@ -1,50 +1,28 @@
use std::sync::Arc; use std::sync::Arc;
use async_graphql::dynamic::{ use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef};
Field, FieldFuture, FieldValue, InputObject, InputValue, Object, TypeRef, use sea_orm::{EntityTrait, QueryFilter};
};
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use util_derive::DynamicGraphql; use util_derive::DynamicGraphql;
use crate::{ use crate::{
app::AppContextTrait, app::AppContextTrait,
auth::AuthUserInfo,
errors::RecorderError, errors::RecorderError,
graphql::{ graphql::{
domains::subscribers::restrict_subscriber_for_entity, domains::subscribers::restrict_subscriber_for_entity,
infra::crypto::{ infra::{
crypto::{
register_crypto_column_input_conversion_to_schema_context, register_crypto_column_input_conversion_to_schema_context,
register_crypto_column_output_conversion_to_schema_context, register_crypto_column_output_conversion_to_schema_context,
}, },
custom::{generate_entity_filtered_mutation_field, register_entity_default_writable},
name::get_entity_custom_mutation_field_name,
},
}, },
models::credential_3rd, models::credential_3rd,
}; };
#[derive(DynamicGraphql, Serialize, Deserialize, Clone, Debug)]
struct Credential3rdCheckAvailableInput {
pub id: i32,
}
impl Credential3rdCheckAvailableInput {
fn input_type_name() -> &'static str {
"Credential3rdCheckAvailableInput"
}
fn arg_name() -> &'static str {
"filter"
}
fn generate_input_object() -> InputObject {
InputObject::new(Self::input_type_name())
.description("The input of the credential3rdCheckAvailable query")
.field(InputValue::new(
Credential3rdCheckAvailableInputFieldEnum::Id.as_str(),
TypeRef::named_nn(TypeRef::INT),
))
}
}
#[derive(DynamicGraphql, Serialize, Deserialize, Clone, Debug)] #[derive(DynamicGraphql, Serialize, Deserialize, Clone, Debug)]
pub struct Credential3rdCheckAvailableInfo { pub struct Credential3rdCheckAvailableInfo {
pub available: bool, pub available: bool,
@@ -117,38 +95,32 @@ pub fn register_credential3rd_to_schema_builder(
mut builder: SeaographyBuilder, mut builder: SeaographyBuilder,
) -> SeaographyBuilder { ) -> SeaographyBuilder {
builder.register_enumeration::<credential_3rd::Credential3rdType>(); builder.register_enumeration::<credential_3rd::Credential3rdType>();
seaography::register_entity!(builder, credential_3rd); builder = register_entity_default_writable!(builder, credential_3rd, false);
builder.schema = builder
.schema
.register(Credential3rdCheckAvailableInput::generate_input_object());
builder.schema = builder builder.schema = builder
.schema .schema
.register(Credential3rdCheckAvailableInfo::generate_output_object()); .register(Credential3rdCheckAvailableInfo::generate_output_object());
builder.queries.push( let builder_context = builder.context;
Field::new( {
"credential3rdCheckAvailable", let check_available_mutation_name = get_entity_custom_mutation_field_name::<
credential_3rd::Entity,
>(builder_context, "CheckAvailable");
let check_available_mutation =
generate_entity_filtered_mutation_field::<credential_3rd::Entity, _, _>(
builder_context,
check_available_mutation_name,
TypeRef::named_nn(Credential3rdCheckAvailableInfo::object_type_name()), TypeRef::named_nn(Credential3rdCheckAvailableInfo::object_type_name()),
move |ctx| { Arc::new(|_resolver_ctx, app_ctx, filters| {
FieldFuture::new(async move { Box::pin(async move {
let auth_user_info = ctx.data::<AuthUserInfo>()?; let db = app_ctx.db();
let input: Credential3rdCheckAvailableInput = ctx
.args
.get(Credential3rdCheckAvailableInput::arg_name())
.unwrap()
.deserialize()?;
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let credential_model = credential_3rd::Model::find_by_id_and_subscriber_id( let credential_model = credential_3rd::Entity::find()
app_ctx.as_ref(), .filter(filters)
input.id, .one(db)
auth_user_info.subscriber_auth.subscriber_id,
)
.await? .await?
.ok_or_else(|| RecorderError::Credential3rdError { .ok_or_else(|| {
message: format!("credential = {} not found", input.id), RecorderError::from_entity_not_found::<credential_3rd::Entity>()
source: None.into(),
})?; })?;
let available = credential_model.check_available(app_ctx.as_ref()).await?; let available = credential_model.check_available(app_ctx.as_ref()).await?;
@@ -156,13 +128,10 @@ pub fn register_credential3rd_to_schema_builder(
Credential3rdCheckAvailableInfo { available }, Credential3rdCheckAvailableInfo { available },
))) )))
}) })
}, }),
)
.argument(InputValue::new(
Credential3rdCheckAvailableInput::arg_name(),
TypeRef::named_nn(Credential3rdCheckAvailableInput::input_type_name()),
)),
); );
builder.mutations.push(check_available_mutation);
}
builder builder
} }

View File

@@ -0,0 +1,71 @@
use convert_case::Case;
use sea_orm::Iterable;
use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{
graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::{
custom::register_entity_default_writable,
json::{
convert_jsonb_output_case_for_entity, restrict_jsonb_filter_input_for_entity,
validate_jsonb_input_for_entity,
},
name::get_entity_and_column_name,
},
},
models::{cron, subscriber_tasks},
};
fn skip_columns_for_entity_input(context: &mut BuilderContext) {
for column in cron::Column::iter() {
if matches!(
column,
cron::Column::SubscriberTask
| cron::Column::Id
| cron::Column::CronExpr
| cron::Column::Enabled
| cron::Column::TimeoutMs
| cron::Column::MaxAttempts
) {
continue;
}
let entity_column_key = get_entity_and_column_name::<cron::Entity>(context, &column);
context.entity_input.insert_skips.push(entity_column_key);
}
for column in cron::Column::iter() {
if matches!(column, |cron::Column::CronExpr| cron::Column::Enabled
| cron::Column::TimeoutMs
| cron::Column::Priority
| cron::Column::MaxAttempts)
{
continue;
}
let entity_column_key = get_entity_and_column_name::<cron::Entity>(context, &column);
context.entity_input.update_skips.push(entity_column_key);
}
}
pub fn register_cron_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<cron::Entity>(context, &cron::Column::SubscriberId);
restrict_jsonb_filter_input_for_entity::<cron::Entity>(context, &cron::Column::SubscriberTask);
convert_jsonb_output_case_for_entity::<cron::Entity>(
context,
&cron::Column::SubscriberTask,
Case::Camel,
);
validate_jsonb_input_for_entity::<cron::Entity, Option<subscriber_tasks::SubscriberTask>>(
context,
&cron::Column::SubscriberTask,
);
skip_columns_for_entity_input(context);
}
pub fn register_cron_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<cron::CronStatus>();
builder = register_entity_default_writable!(builder, cron, true);
builder
}

View File

@@ -1,6 +1,12 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::downloaders}; use crate::{
graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::downloaders,
};
pub fn register_downloaders_to_schema_context(context: &mut BuilderContext) { pub fn register_downloaders_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<downloaders::Entity>( restrict_subscriber_for_entity::<downloaders::Entity>(
@@ -11,7 +17,7 @@ pub fn register_downloaders_to_schema_context(context: &mut BuilderContext) {
pub fn register_downloaders_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { pub fn register_downloaders_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<downloaders::DownloaderCategory>(); builder.register_enumeration::<downloaders::DownloaderCategory>();
seaography::register_entity!(builder, downloaders); builder = register_entity_default_writable!(builder, downloaders, false);
builder builder
} }

View File

@@ -1,6 +1,12 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::downloads}; use crate::{
graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::downloads,
};
pub fn register_downloads_to_schema_context(context: &mut BuilderContext) { pub fn register_downloads_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<downloads::Entity>(context, &downloads::Column::SubscriberId); restrict_subscriber_for_entity::<downloads::Entity>(context, &downloads::Column::SubscriberId);
@@ -9,7 +15,7 @@ pub fn register_downloads_to_schema_context(context: &mut BuilderContext) {
pub fn register_downloads_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { pub fn register_downloads_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<downloads::DownloadStatus>(); builder.register_enumeration::<downloads::DownloadStatus>();
builder.register_enumeration::<downloads::DownloadMime>(); builder.register_enumeration::<downloads::DownloadMime>();
seaography::register_entity!(builder, downloads); builder = register_entity_default_writable!(builder, downloads, false);
builder builder
} }

View File

@@ -1,6 +1,12 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::episodes}; use crate::{
graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::episodes,
};
pub fn register_episodes_to_schema_context(context: &mut BuilderContext) { pub fn register_episodes_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<episodes::Entity>(context, &episodes::Column::SubscriberId); restrict_subscriber_for_entity::<episodes::Entity>(context, &episodes::Column::SubscriberId);
@@ -8,7 +14,7 @@ pub fn register_episodes_to_schema_context(context: &mut BuilderContext) {
pub fn register_episodes_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { pub fn register_episodes_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<episodes::EpisodeType>(); builder.register_enumeration::<episodes::EpisodeType>();
seaography::register_entity!(builder, episodes); builder = register_entity_default_writable!(builder, episodes, false);
builder builder
} }

View File

@@ -7,7 +7,13 @@ use seaography::{Builder as SeaographyBuilder, BuilderContext, SeaResult};
use crate::{ use crate::{
graphql::{ graphql::{
domains::subscribers::restrict_subscriber_for_entity, domains::subscribers::restrict_subscriber_for_entity,
infra::util::{get_entity_column_key, get_entity_key}, infra::{
custom::register_entity_default_writable,
name::{
get_entity_and_column_name, get_entity_create_batch_mutation_field_name,
get_entity_create_one_mutation_field_name,
},
},
}, },
models::feeds, models::feeds,
}; };
@@ -15,22 +21,14 @@ use crate::{
pub fn register_feeds_to_schema_context(context: &mut BuilderContext) { pub fn register_feeds_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<feeds::Entity>(context, &feeds::Column::SubscriberId); restrict_subscriber_for_entity::<feeds::Entity>(context, &feeds::Column::SubscriberId);
{ {
let entity_column_key = let entity_create_one_mutation_field_name = Arc::new(
get_entity_column_key::<feeds::Entity>(context, &feeds::Column::Token); get_entity_create_one_mutation_field_name::<feeds::Entity>(context),
let entity_key = get_entity_key::<feeds::Entity>(context); );
let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); let entity_create_batch_mutation_field_name =
let entity_create_one_mutation_field_name = Arc::new(format!( Arc::new(get_entity_create_batch_mutation_field_name::<feeds::Entity>(context));
"{}{}",
entity_name, context.entity_create_one_mutation.mutation_suffix
));
let entity_create_batch_mutation_field_name = Arc::new(format!(
"{}{}",
entity_name,
context.entity_create_batch_mutation.mutation_suffix.clone()
));
context.types.input_none_conversions.insert( context.types.input_none_conversions.insert(
entity_column_key, get_entity_and_column_name::<feeds::Entity>(context, &feeds::Column::Token),
Box::new( Box::new(
move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> { move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> {
let field_name = context.field().name(); let field_name = context.field().name();
@@ -50,7 +48,8 @@ pub fn register_feeds_to_schema_context(context: &mut BuilderContext) {
pub fn register_feeds_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { pub fn register_feeds_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
builder.register_enumeration::<feeds::FeedType>(); builder.register_enumeration::<feeds::FeedType>();
builder.register_enumeration::<feeds::FeedSource>(); builder.register_enumeration::<feeds::FeedSource>();
seaography::register_entity!(builder, feeds);
builder = register_entity_default_writable!(builder, feeds, false);
builder builder
} }

View File

@@ -10,3 +10,4 @@ pub mod subscribers;
pub mod subscription_bangumi; pub mod subscription_bangumi;
pub mod subscription_episode; pub mod subscription_episode;
pub mod subscriptions; pub mod subscriptions;
pub mod cron;

View File

@@ -1,121 +1,85 @@
use std::{ops::Deref, sync::Arc}; use std::{ops::Deref, sync::Arc};
use async_graphql::dynamic::{FieldValue, TypeRef}; use async_graphql::dynamic::{FieldValue, TypeRef, ValueAccessor};
use convert_case::Case;
use sea_orm::{ use sea_orm::{
ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QuerySelect, QueryTrait, prelude::Expr, ColumnTrait, ConnectionTrait, EntityTrait, Iterable, QueryFilter, QuerySelect, QueryTrait,
sea_query::Query, prelude::Expr, sea_query::Query,
};
use seaography::{
Builder as SeaographyBuilder, BuilderContext, EntityDeleteMutationBuilder, EntityObjectBuilder,
EntityQueryFieldBuilder, get_filter_conditions,
}; };
use seaography::{Builder as SeaographyBuilder, BuilderContext, GuardAction};
use crate::{ use crate::{
auth::AuthUserInfo,
errors::RecorderError, errors::RecorderError,
graphql::{ graphql::{
domains::subscribers::restrict_subscriber_for_entity, domains::subscribers::restrict_subscriber_for_entity,
infra::{ infra::{
custom::generate_entity_filter_mutation_field, custom::{
json::{convert_jsonb_output_case_for_entity, restrict_jsonb_filter_input_for_entity}, generate_entity_create_one_mutation_field,
generate_entity_default_basic_entity_object,
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,
},
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,
},
}, },
}, },
models::subscriber_tasks, models::subscriber_tasks,
task::{ApalisJobs, ApalisSchema}, task::{ApalisJobs, ApalisSchema},
}; };
pub fn register_subscriber_tasks_entity_mutations( pub fn check_entity_and_task_subscriber_id_matches(
mut builder: SeaographyBuilder, value_accessor: &ValueAccessor<'_>,
) -> SeaographyBuilder { subscriber_id: i32,
let context = builder.context; subscriber_id_column_name: &str,
{ subscriber_task_column_name: &str,
let entitity_delete_mutation_builder = EntityDeleteMutationBuilder { context }; ) -> bool {
let delete_mutation = generate_entity_filter_mutation_field::<subscriber_tasks::Entity, _, _>( value_accessor.object().is_ok_and(|input_object| {
context, input_object
entitity_delete_mutation_builder.type_name::<subscriber_tasks::Entity>(), .get(subscriber_task_column_name)
TypeRef::named_nn(TypeRef::INT), .and_then(|subscriber_task_value| subscriber_task_value.object().ok())
Arc::new(|resolver_ctx, app_ctx, filters| { .and_then(|subscriber_task_object| {
let filters_condition = get_filter_conditions::<subscriber_tasks::Entity>( subscriber_task_object
resolver_ctx, .get("subscriber_id")
context, .and_then(|job_subscriber_id| job_subscriber_id.i64().ok())
filters,
);
Box::pin(async move {
let db = app_ctx.db();
let select_subquery = subscriber_tasks::Entity::find()
.select_only()
.column(subscriber_tasks::Column::Id)
.filter(filters_condition);
let delete_query = Query::delete()
.from_table((ApalisSchema::Schema, ApalisJobs::Table))
.and_where(
Expr::col(ApalisJobs::Id).in_subquery(select_subquery.into_query()),
)
.to_owned();
let db_backend = db.deref().get_database_backend();
let delete_statement = db_backend.build(&delete_query);
let result = db.execute(delete_statement).await?;
Ok::<_, RecorderError>(Some(FieldValue::value(result.rows_affected() as i32)))
}) })
}), .is_some_and(|subscriber_task_subscriber_id| {
); subscriber_task_subscriber_id as i32
builder.mutations.push(delete_mutation); == input_object
} .get(subscriber_id_column_name)
{ .and_then(|subscriber_id_object| subscriber_id_object.i64().ok())
let entity_object_builder = EntityObjectBuilder { context }; .map(|subscriber_id| subscriber_id as i32)
let entity_query_field = EntityQueryFieldBuilder { context }; .unwrap_or(subscriber_id)
let entity_retry_one_mutation_name = format!(
"{}RetryOne",
entity_query_field.type_name::<subscriber_tasks::Entity>()
);
let retry_one_mutation =
generate_entity_filter_mutation_field::<subscriber_tasks::Entity, _, _>(
context,
entity_retry_one_mutation_name,
TypeRef::named_nn(entity_object_builder.type_name::<subscriber_tasks::Entity>()),
Arc::new(|resolver_ctx, app_ctx, filters| {
let filters_condition = get_filter_conditions::<subscriber_tasks::Entity>(
resolver_ctx,
context,
filters,
);
Box::pin(async move {
let db = app_ctx.db();
let job_id = subscriber_tasks::Entity::find()
.filter(filters_condition)
.select_only()
.column(subscriber_tasks::Column::Id)
.into_tuple::<String>()
.one(db)
.await?
.ok_or_else(|| RecorderError::ModelEntityNotFound {
entity: "SubscriberTask".into(),
})?;
let task = app_ctx.task();
task.retry_subscriber_task(job_id.clone()).await?;
let task_model = subscriber_tasks::Entity::find()
.filter(subscriber_tasks::Column::Id.eq(&job_id))
.one(db)
.await?
.ok_or_else(|| RecorderError::ModelEntityNotFound {
entity: "SubscriberTask".into(),
})?;
Ok::<_, RecorderError>(Some(FieldValue::owned_any(task_model)))
}) })
}), })
); }
builder.mutations.push(retry_one_mutation);
}
builder 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
) {
continue;
}
let entity_column_key =
get_entity_and_column_name::<subscriber_tasks::Entity>(context, &column);
context.entity_input.insert_skips.push(entity_column_key);
}
} }
pub fn register_subscriber_tasks_to_schema_context(context: &mut BuilderContext) { pub fn register_subscriber_tasks_to_schema_context(context: &mut BuilderContext) {
@@ -130,22 +94,224 @@ pub fn register_subscriber_tasks_to_schema_context(context: &mut BuilderContext)
convert_jsonb_output_case_for_entity::<subscriber_tasks::Entity>( convert_jsonb_output_case_for_entity::<subscriber_tasks::Entity>(
context, context,
&subscriber_tasks::Column::Job, &subscriber_tasks::Column::Job,
Case::Camel,
);
validate_jsonb_input_for_entity::<subscriber_tasks::Entity, subscriber_tasks::SubscriberTask>(
context,
&subscriber_tasks::Column::Job,
);
skip_columns_for_entity_input(context);
context.guards.field_guards.insert(
get_entity_and_column_name::<subscriber_tasks::Entity>(
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::<subscriber_tasks::Entity>(
context,
&subscriber_tasks::Column::Job,
));
let subscriber_id_column_name = Arc::new(get_column_name::<subscriber_tasks::Entity>(
context,
&subscriber_tasks::Column::SubscriberId,
));
Box::new(move |resolve_context| {
let field_name = resolve_context.field().name();
let subscriber_id = resolve_context
.data_opt::<AuthUserInfo>()
.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( pub fn register_subscriber_tasks_to_schema_builder(
mut builder: SeaographyBuilder, mut builder: SeaographyBuilder,
) -> SeaographyBuilder { ) -> SeaographyBuilder {
builder.register_entity::<subscriber_tasks::Entity>(
<subscriber_tasks::RelatedEntity as sea_orm::Iterable>::iter()
.map(|rel| seaography::RelationBuilder::get_relation(&rel, builder.context))
.collect(),
);
builder = builder.register_entity_dataloader_one_to_one(subscriber_tasks::Entity, tokio::spawn);
builder =
builder.register_entity_dataloader_one_to_many(subscriber_tasks::Entity, tokio::spawn);
builder = register_subscriber_tasks_entity_mutations(builder);
builder.register_enumeration::<subscriber_tasks::SubscriberTaskType>(); builder.register_enumeration::<subscriber_tasks::SubscriberTaskType>();
builder.register_enumeration::<subscriber_tasks::SubscriberTaskStatus>(); builder.register_enumeration::<subscriber_tasks::SubscriberTaskStatus>();
builder = register_entity_default_readonly!(builder, subscriber_tasks);
let builder_context = builder.context;
{
builder
.outputs
.push(generate_entity_default_basic_entity_object::<
subscriber_tasks::Entity,
>(builder_context));
}
{
let delete_mutation = generate_entity_delete_mutation_field::<subscriber_tasks::Entity>(
builder_context,
Arc::new(|_resolver_ctx, app_ctx, filters| {
Box::pin(async move {
let db = app_ctx.db();
let select_subquery = subscriber_tasks::Entity::find()
.select_only()
.column(subscriber_tasks::Column::Id)
.filter(filters);
let delete_query = Query::delete()
.from_table((ApalisSchema::Schema, ApalisJobs::Table))
.and_where(
Expr::col(ApalisJobs::Id).in_subquery(select_subquery.into_query()),
)
.to_owned();
let db_backend = db.deref().get_database_backend();
let delete_statement = db_backend.build(&delete_query);
let result = db.execute(delete_statement).await?;
Ok::<_, RecorderError>(result.rows_affected())
})
}),
);
builder.mutations.push(delete_mutation);
}
{
let entity_retry_one_mutation_name = get_entity_custom_mutation_field_name::<
subscriber_tasks::Entity,
>(builder_context, "RetryOne");
let retry_one_mutation =
generate_entity_filtered_mutation_field::<subscriber_tasks::Entity, _, _>(
builder_context,
entity_retry_one_mutation_name,
TypeRef::named_nn(get_entity_basic_type_name::<subscriber_tasks::Entity>(
builder_context,
)),
Arc::new(|_resolver_ctx, app_ctx, filters| {
Box::pin(async move {
let db = app_ctx.db();
let job_id = subscriber_tasks::Entity::find()
.filter(filters)
.select_only()
.column(subscriber_tasks::Column::Id)
.into_tuple::<String>()
.one(db)
.await?
.ok_or_else(|| {
RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?;
let task = app_ctx.task();
task.retry_subscriber_task(job_id.clone()).await?;
let task_model = subscriber_tasks::Entity::find()
.filter(subscriber_tasks::Column::Id.eq(&job_id))
.one(db)
.await?
.ok_or_else(|| {
RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?;
Ok::<_, RecorderError>(Some(FieldValue::owned_any(task_model)))
})
}),
);
builder.mutations.push(retry_one_mutation);
}
{
builder
.inputs
.push(generate_entity_default_insert_input_object::<
subscriber_tasks::Entity,
>(builder_context));
let create_one_mutation =
generate_entity_create_one_mutation_field::<subscriber_tasks::Entity, TypeRef>(
builder_context,
None,
Arc::new(|_resolver_ctx, app_ctx, input_object| {
let job_column_name = get_column_name::<subscriber_tasks::Entity>(
builder_context,
&subscriber_tasks::Column::Job,
);
let task = input_object
.get(job_column_name.as_str())
.unwrap()
.deserialize::<subscriber_tasks::SubscriberTask>()
.unwrap();
Box::pin(async move {
let task_service = app_ctx.task();
let task_id = task_service.add_subscriber_task(task).await?.to_string();
let db = app_ctx.db();
let task = subscriber_tasks::Entity::find()
.filter(subscriber_tasks::Column::Id.eq(&task_id))
.one(db)
.await?
.ok_or_else(|| {
RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?;
Ok::<_, RecorderError>(task)
})
}),
);
builder.mutations.push(create_one_mutation);
}
builder builder
} }

View File

@@ -12,7 +12,17 @@ use seaography::{
use crate::{ use crate::{
auth::{AuthError, AuthUserInfo}, auth::{AuthError, AuthUserInfo},
graphql::infra::util::{get_column_key, get_entity_column_key, get_entity_key}, graphql::infra::{
custom::register_entity_default_readonly,
name::{
get_column_name, get_entity_and_column_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_name,
get_entity_update_mutation_data_field_name, get_entity_update_mutation_field_name,
},
},
models::subscribers, models::subscribers,
}; };
@@ -82,32 +92,19 @@ where
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_key = get_entity_key::<T>(context); let column_name = Arc::new(get_column_name::<T>(context, column));
let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); let entity_create_one_mutation_field_name =
let column_key = get_column_key::<T>(context, column); Arc::new(get_entity_create_one_mutation_field_name::<T>(context));
let column_name = Arc::new(context.entity_object.column_name.as_ref()(
&entity_key,
&column_key,
));
let entity_create_one_mutation_field_name = Arc::new(format!(
"{}{}",
entity_name, context.entity_create_one_mutation.mutation_suffix
));
let entity_create_one_mutation_data_field_name = let entity_create_one_mutation_data_field_name =
Arc::new(context.entity_create_one_mutation.data_field.clone()); Arc::new(get_entity_create_one_mutation_data_field_name(context).to_string());
let entity_create_batch_mutation_field_name = Arc::new(format!( let entity_create_batch_mutation_field_name =
"{}{}", Arc::new(get_entity_create_batch_mutation_field_name::<T>(context));
entity_name,
context.entity_create_batch_mutation.mutation_suffix.clone()
));
let entity_create_batch_mutation_data_field_name = let entity_create_batch_mutation_data_field_name =
Arc::new(context.entity_create_batch_mutation.data_field.clone()); Arc::new(get_entity_create_batch_mutation_data_field_name(context).to_string());
let entity_update_mutation_field_name = Arc::new(format!( let entity_update_mutation_field_name =
"{}{}", Arc::new(get_entity_update_mutation_field_name::<T>(context));
entity_name, context.entity_update_mutation.mutation_suffix
));
let entity_update_mutation_data_field_name = let entity_update_mutation_data_field_name =
Arc::new(context.entity_update_mutation.data_field.clone()); Arc::new(get_entity_update_mutation_data_field_name(context).to_string());
Box::new(move |context: &ResolverContext| -> GuardAction { Box::new(move |context: &ResolverContext| -> GuardAction {
match context.ctx.data::<AuthUserInfo>() { match context.ctx.data::<AuthUserInfo>() {
@@ -253,17 +250,10 @@ where
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_key = get_entity_key::<T>(context); let entity_create_one_mutation_field_name =
let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); Arc::new(get_entity_create_one_mutation_field_name::<T>(context));
let entity_create_one_mutation_field_name = Arc::new(format!( let entity_create_batch_mutation_field_name =
"{}{}", Arc::new(get_entity_create_batch_mutation_field_name::<T>(context));
entity_name, context.entity_create_one_mutation.mutation_suffix
));
let entity_create_batch_mutation_field_name = Arc::new(format!(
"{}{}",
entity_name,
context.entity_create_batch_mutation.mutation_suffix.clone()
));
Box::new( Box::new(
move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> { move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> {
let field_name = context.field().name(); let field_name = context.field().name();
@@ -289,40 +279,39 @@ where
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_key = get_entity_key::<T>(context); let entity_and_column = get_entity_and_column_name::<T>(context, column);
let entity_column_key = get_entity_column_key::<T>(context, column);
context.guards.entity_guards.insert( context.guards.entity_guards.insert(
entity_key.clone(), get_entity_name::<T>(context),
guard_entity_with_subscriber_id::<T>(context, column), guard_entity_with_subscriber_id::<T>(context, column),
); );
context.guards.field_guards.insert( context.guards.field_guards.insert(
entity_column_key.clone(), get_entity_and_column_name::<T>(context, column),
guard_field_with_subscriber_id::<T>(context, column), guard_field_with_subscriber_id::<T>(context, column),
); );
context.filter_types.overwrites.insert( context.filter_types.overwrites.insert(
entity_column_key.clone(), get_entity_and_column_name::<T>(context, column),
Some(FilterType::Custom( Some(FilterType::Custom(
SUBSCRIBER_ID_FILTER_INFO.type_name.clone(), SUBSCRIBER_ID_FILTER_INFO.type_name.clone(),
)), )),
); );
context.filter_types.condition_functions.insert( context.filter_types.condition_functions.insert(
entity_column_key.clone(), entity_and_column.clone(),
generate_subscriber_id_filter_condition::<T>(context, column), generate_subscriber_id_filter_condition::<T>(context, column),
); );
context.types.input_none_conversions.insert( context.types.input_none_conversions.insert(
entity_column_key.clone(), entity_and_column.clone(),
generate_default_subscriber_id_input_conversion::<T>(context, column), generate_default_subscriber_id_input_conversion::<T>(context, column),
); );
context.entity_input.update_skips.push(entity_column_key); context.entity_input.update_skips.push(entity_and_column);
} }
pub fn register_subscribers_to_schema_context(context: &mut BuilderContext) { pub fn register_subscribers_to_schema_context(context: &mut BuilderContext) {
restrict_subscriber_for_entity::<subscribers::Entity>(context, &subscribers::Column::Id); restrict_subscriber_for_entity::<subscribers::Entity>(context, &subscribers::Column::Id);
for column in subscribers::Column::iter() { for column in subscribers::Column::iter() {
if !matches!(column, subscribers::Column::Id) { if !matches!(column, subscribers::Column::Id) {
let key = get_entity_column_key::<subscribers::Entity>(context, &column); let key = get_entity_and_column_name::<subscribers::Entity>(context, &column);
context.filter_types.overwrites.insert(key, None); context.filter_types.overwrites.insert(key, None);
} }
} }
@@ -339,15 +328,7 @@ pub fn register_subscribers_to_schema_builder(mut builder: SeaographyBuilder) ->
.register(filter_types_map_helper.generate_filter_input(&SUBSCRIBER_ID_FILTER_INFO)); .register(filter_types_map_helper.generate_filter_input(&SUBSCRIBER_ID_FILTER_INFO));
} }
{ builder = register_entity_default_readonly!(builder, subscribers);
builder.register_entity::<subscribers::Entity>(
<subscribers::RelatedEntity as sea_orm::Iterable>::iter()
.map(|rel| seaography::RelationBuilder::get_relation(&rel, builder.context))
.collect(),
);
builder = builder.register_entity_dataloader_one_to_one(subscribers::Entity, tokio::spawn);
builder = builder.register_entity_dataloader_one_to_many(subscribers::Entity, tokio::spawn);
}
builder builder
} }

View File

@@ -1,7 +1,11 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{ use crate::{
graphql::domains::subscribers::restrict_subscriber_for_entity, models::subscription_bangumi, graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::subscription_bangumi,
}; };
pub fn register_subscription_bangumi_to_schema_context(context: &mut BuilderContext) { pub fn register_subscription_bangumi_to_schema_context(context: &mut BuilderContext) {
@@ -14,7 +18,7 @@ pub fn register_subscription_bangumi_to_schema_context(context: &mut BuilderCont
pub fn register_subscription_bangumi_to_schema_builder( pub fn register_subscription_bangumi_to_schema_builder(
mut builder: SeaographyBuilder, mut builder: SeaographyBuilder,
) -> SeaographyBuilder { ) -> SeaographyBuilder {
seaography::register_entity!(builder, subscription_bangumi); builder = register_entity_default_writable!(builder, subscription_bangumi, false);
builder builder
} }

View File

@@ -1,7 +1,11 @@
use seaography::{Builder as SeaographyBuilder, BuilderContext}; use seaography::{Builder as SeaographyBuilder, BuilderContext};
use crate::{ use crate::{
graphql::domains::subscribers::restrict_subscriber_for_entity, models::subscription_episode, graphql::{
domains::subscribers::restrict_subscriber_for_entity,
infra::custom::register_entity_default_writable,
},
models::subscription_episode,
}; };
pub fn register_subscription_episode_to_schema_context(context: &mut BuilderContext) { pub fn register_subscription_episode_to_schema_context(context: &mut BuilderContext) {
@@ -14,7 +18,7 @@ pub fn register_subscription_episode_to_schema_context(context: &mut BuilderCont
pub fn register_subscription_episode_to_schema_builder( pub fn register_subscription_episode_to_schema_builder(
mut builder: SeaographyBuilder, mut builder: SeaographyBuilder,
) -> SeaographyBuilder { ) -> SeaographyBuilder {
seaography::register_entity!(builder, subscription_episode); builder = register_entity_default_writable!(builder, subscription_episode, false);
builder builder
} }

View File

@@ -2,22 +2,22 @@ use std::sync::Arc;
use async_graphql::dynamic::{FieldValue, TypeRef}; use async_graphql::dynamic::{FieldValue, TypeRef};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use seaography::{ use seaography::{Builder as SeaographyBuilder, BuilderContext};
Builder as SeaographyBuilder, BuilderContext, EntityObjectBuilder, EntityQueryFieldBuilder,
get_filter_conditions,
};
use crate::{ use crate::{
errors::RecorderError, errors::RecorderError,
graphql::{ graphql::{
domains::subscribers::restrict_subscriber_for_entity, domains::subscribers::restrict_subscriber_for_entity,
infra::custom::generate_entity_filter_mutation_field, infra::{
custom::{generate_entity_filtered_mutation_field, register_entity_default_writable},
name::{get_entity_basic_type_name, get_entity_custom_mutation_field_name},
}, },
models::{
subscriber_tasks,
subscriptions::{self, SubscriptionTrait},
}, },
task::SubscriberTask, models::{subscriber_tasks, subscriptions},
task::{
SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask,
SyncOneSubscriptionSourcesTask,
},
}; };
pub fn register_subscriptions_to_schema_context(context: &mut BuilderContext) { pub fn register_subscriptions_to_schema_context(context: &mut BuilderContext) {
@@ -31,53 +31,43 @@ pub fn register_subscriptions_to_schema_builder(
mut builder: SeaographyBuilder, mut builder: SeaographyBuilder,
) -> SeaographyBuilder { ) -> SeaographyBuilder {
builder.register_enumeration::<subscriptions::SubscriptionCategory>(); builder.register_enumeration::<subscriptions::SubscriptionCategory>();
seaography::register_entity!(builder, subscriptions); builder = register_entity_default_writable!(builder, subscriptions, false);
let context = builder.context; let context = builder.context;
let entity_object_builder = EntityObjectBuilder { context };
let entity_query_field = EntityQueryFieldBuilder { context };
{ {
let sync_one_feeds_incremental_mutation_name = format!( let sync_one_feeds_incremental_mutation_name = get_entity_custom_mutation_field_name::<
"{}SyncOneFeedsIncremental",
entity_query_field.type_name::<subscriptions::Entity>()
);
let sync_one_feeds_incremental_mutation = generate_entity_filter_mutation_field::<
subscriptions::Entity, subscriptions::Entity,
_, >(context, "SyncOneFeedsIncremental");
_,
>( let sync_one_feeds_incremental_mutation =
generate_entity_filtered_mutation_field::<subscriptions::Entity, _, _>(
builder.context, builder.context,
sync_one_feeds_incremental_mutation_name, sync_one_feeds_incremental_mutation_name,
TypeRef::named_nn(entity_object_builder.type_name::<subscriber_tasks::Entity>()), TypeRef::named_nn(get_entity_basic_type_name::<subscriber_tasks::Entity>(
Arc::new(|resolver_ctx, app_ctx, filters| { context,
let filters_condition = )),
get_filter_conditions::<subscriptions::Entity>(resolver_ctx, context, filters); Arc::new(|_resolver_ctx, app_ctx, filters| {
Box::pin(async move { Box::pin(async move {
let db = app_ctx.db(); let db = app_ctx.db();
let subscription_model = subscriptions::Entity::find() let subscription_model = subscriptions::Entity::find()
.filter(filters_condition) .filter(filters)
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "Subscription".into(), RecorderError::from_entity_not_found::<subscriptions::Entity>()
})?; })?;
let subscription =
subscriptions::Subscription::try_from_model(&subscription_model)?;
let task_service = app_ctx.task(); let task_service = app_ctx.task();
let task_id = task_service let task_id = task_service
.add_subscriber_task( .add_subscriber_task(
subscription_model.subscriber_id, SyncOneSubscriptionFeedsIncrementalTask::builder()
SubscriberTask::SyncOneSubscriptionFeedsIncremental( .subscriber_id(subscription_model.subscriber_id)
subscription.into(), .subscription_id(subscription_model.id)
), .build()
.into(),
) )
.await?; .await?;
@@ -85,8 +75,8 @@ pub fn register_subscriptions_to_schema_builder(
.filter(subscriber_tasks::Column::Id.eq(task_id.to_string())) .filter(subscriber_tasks::Column::Id.eq(task_id.to_string()))
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "SubscriberTask".into(), RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?; })?;
Ok(Some(FieldValue::owned_any(task_model))) Ok(Some(FieldValue::owned_any(task_model)))
@@ -97,43 +87,37 @@ pub fn register_subscriptions_to_schema_builder(
builder.mutations.push(sync_one_feeds_incremental_mutation); builder.mutations.push(sync_one_feeds_incremental_mutation);
} }
{ {
let sync_one_feeds_full_mutation_name = format!( let sync_one_feeds_full_mutation_name = get_entity_custom_mutation_field_name::<
"{}SyncOneFeedsFull",
entity_query_field.type_name::<subscriptions::Entity>()
);
let sync_one_feeds_full_mutation = generate_entity_filter_mutation_field::<
subscriptions::Entity, subscriptions::Entity,
_, >(builder.context, "SyncOneFeedsFull");
_, let sync_one_feeds_full_mutation =
>( generate_entity_filtered_mutation_field::<subscriptions::Entity, _, _>(
builder.context, builder.context,
sync_one_feeds_full_mutation_name, sync_one_feeds_full_mutation_name,
TypeRef::named_nn(entity_object_builder.type_name::<subscriber_tasks::Entity>()), TypeRef::named_nn(get_entity_basic_type_name::<subscriber_tasks::Entity>(
Arc::new(|resolver_ctx, app_ctx, filters| { context,
let filters_condition = )),
get_filter_conditions::<subscriptions::Entity>(resolver_ctx, context, filters); Arc::new(|_resolver_ctx, app_ctx, filters| {
Box::pin(async move { Box::pin(async move {
let db = app_ctx.db(); let db = app_ctx.db();
let subscription_model = subscriptions::Entity::find() let subscription_model = subscriptions::Entity::find()
.filter(filters_condition) .filter(filters)
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "Subscription".into(), RecorderError::from_entity_not_found::<subscriptions::Entity>()
})?; })?;
let subscription =
subscriptions::Subscription::try_from_model(&subscription_model)?;
let task_service = app_ctx.task(); let task_service = app_ctx.task();
let task_id = task_service let task_id = task_service
.add_subscriber_task( .add_subscriber_task(
subscription_model.subscriber_id, SyncOneSubscriptionFeedsFullTask::builder()
SubscriberTask::SyncOneSubscriptionFeedsFull(subscription.into()), .subscriber_id(subscription_model.subscriber_id)
.subscription_id(subscription_model.id)
.build()
.into(),
) )
.await?; .await?;
@@ -141,8 +125,8 @@ pub fn register_subscriptions_to_schema_builder(
.filter(subscriber_tasks::Column::Id.eq(task_id.to_string())) .filter(subscriber_tasks::Column::Id.eq(task_id.to_string()))
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "SubscriberTask".into(), RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?; })?;
Ok(Some(FieldValue::owned_any(task_model))) Ok(Some(FieldValue::owned_any(task_model)))
@@ -154,43 +138,38 @@ pub fn register_subscriptions_to_schema_builder(
} }
{ {
let sync_one_sources_mutation_name = format!( let sync_one_sources_mutation_name = get_entity_custom_mutation_field_name::<
"{}SyncOneSources",
entity_query_field.type_name::<subscriptions::Entity>()
);
let sync_one_sources_mutation = generate_entity_filter_mutation_field::<
subscriptions::Entity, subscriptions::Entity,
_, >(context, "SyncOneSources");
_,
>( let sync_one_sources_mutation =
generate_entity_filtered_mutation_field::<subscriptions::Entity, _, _>(
builder.context, builder.context,
sync_one_sources_mutation_name, sync_one_sources_mutation_name,
TypeRef::named_nn(entity_object_builder.type_name::<subscriber_tasks::Entity>()), TypeRef::named_nn(get_entity_basic_type_name::<subscriber_tasks::Entity>(
Arc::new(|resolver_ctx, app_ctx, filters| { context,
let filters_condition = )),
get_filter_conditions::<subscriptions::Entity>(resolver_ctx, context, filters); Arc::new(|_resolver_ctx, app_ctx, filters| {
Box::pin(async move { Box::pin(async move {
let db = app_ctx.db(); let db = app_ctx.db();
let subscription_model = subscriptions::Entity::find() let subscription_model = subscriptions::Entity::find()
.filter(filters_condition) .filter(filters)
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "Subscription".into(), RecorderError::from_entity_not_found::<subscriptions::Entity>()
})?; })?;
let subscription =
subscriptions::Subscription::try_from_model(&subscription_model)?;
let task_service = app_ctx.task(); let task_service = app_ctx.task();
let task_id = task_service let task_id = task_service
.add_subscriber_task( .add_subscriber_task(
subscription_model.subscriber_id, SyncOneSubscriptionSourcesTask::builder()
SubscriberTask::SyncOneSubscriptionSources(subscription.into()), .subscriber_id(subscription_model.subscriber_id)
.subscription_id(subscription_model.id)
.build()
.into(),
) )
.await?; .await?;
@@ -198,8 +177,8 @@ pub fn register_subscriptions_to_schema_builder(
.filter(subscriber_tasks::Column::Id.eq(task_id.to_string())) .filter(subscriber_tasks::Column::Id.eq(task_id.to_string()))
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::ModelEntityNotFound { .ok_or_else(|| {
entity: "SubscriberTask".into(), RecorderError::from_entity_not_found::<subscriber_tasks::Entity>()
})?; })?;
Ok(Some(FieldValue::owned_any(task_model))) Ok(Some(FieldValue::owned_any(task_model)))

View File

@@ -4,10 +4,7 @@ use async_graphql::dynamic::{ResolverContext, ValueAccessor};
use sea_orm::{EntityTrait, Value as SeaValue}; use sea_orm::{EntityTrait, Value as SeaValue};
use seaography::{BuilderContext, SeaResult}; use seaography::{BuilderContext, SeaResult};
use crate::{ use crate::{app::AppContextTrait, graphql::infra::name::get_entity_and_column_name};
app::AppContextTrait,
graphql::infra::util::{get_column_key, get_entity_key},
};
pub fn register_crypto_column_input_conversion_to_schema_context<T>( pub fn register_crypto_column_input_conversion_to_schema_context<T>(
context: &mut BuilderContext, context: &mut BuilderContext,
@@ -17,13 +14,8 @@ pub fn register_crypto_column_input_conversion_to_schema_context<T>(
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_key = get_entity_key::<T>(context);
let column_name = get_column_key::<T>(context, column);
let entity_name = context.entity_object.type_name.as_ref()(&entity_key);
let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name);
context.types.input_conversions.insert( context.types.input_conversions.insert(
format!("{entity_name}.{column_name}"), get_entity_and_column_name::<T>(context, column),
Box::new( Box::new(
move |_resolve_context: &ResolverContext<'_>, move |_resolve_context: &ResolverContext<'_>,
value: &ValueAccessor| value: &ValueAccessor|
@@ -44,13 +36,8 @@ pub fn register_crypto_column_output_conversion_to_schema_context<T>(
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_key = get_entity_key::<T>(context);
let column_name = get_column_key::<T>(context, column);
let entity_name = context.entity_object.type_name.as_ref()(&entity_key);
let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name);
context.types.output_conversions.insert( context.types.output_conversions.insert(
format!("{entity_name}.{column_name}"), get_entity_and_column_name::<T>(context, column),
Box::new( Box::new(
move |value: &sea_orm::Value| -> SeaResult<async_graphql::Value> { move |value: &sea_orm::Value| -> SeaResult<async_graphql::Value> {
if let SeaValue::String(s) = value { if let SeaValue::String(s) = value {

View File

@@ -1,45 +1,140 @@
use std::{pin::Pin, sync::Arc}; use std::{iter::FusedIterator, pin::Pin, sync::Arc};
use async_graphql::dynamic::{ use async_graphql::dynamic::{
Field, FieldFuture, FieldValue, InputValue, ResolverContext, TypeRef, ValueAccessor, Field, FieldFuture, FieldValue, InputObject, InputValue, Object, ObjectAccessor,
ResolverContext, TypeRef,
};
use sea_orm::{
ActiveModelTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter, TransactionTrait,
};
use seaography::{
Builder as SeaographyBuilder, BuilderContext, GuardAction, RelationBuilder, SeaographyError,
get_filter_conditions, prepare_active_model,
}; };
use sea_orm::EntityTrait;
use seaography::{BuilderContext, EntityObjectBuilder, FilterInputBuilder, GuardAction};
use crate::{app::AppContextTrait, errors::RecorderResult}; use crate::{
app::AppContextTrait,
errors::RecorderResult,
graphql::infra::name::{
get_entity_and_column_name_from_column_str, 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_delete_mutation_field_name, get_entity_delete_mutation_filter_field_name,
get_entity_filter_input_type_name, get_entity_insert_data_input_type_name, get_entity_name,
get_entity_renormalized_filter_field_name, get_entity_update_data_input_type_name,
get_entity_update_mutation_data_field_name, get_entity_update_mutation_field_name,
get_entity_update_mutation_filter_field_name,
},
};
pub type FilterMutationFn = Arc< pub type FilterMutationFn = Arc<
dyn for<'a> Fn( dyn for<'a> Fn(
&ResolverContext<'a>, &ResolverContext<'a>,
Arc<dyn AppContextTrait>, Arc<dyn AppContextTrait>,
Option<ValueAccessor<'_>>, Condition,
) -> Pin< ) -> Pin<
Box<dyn Future<Output = RecorderResult<Option<FieldValue<'a>>>> + Send + 'a>, Box<dyn Future<Output = RecorderResult<Option<FieldValue<'a>>>> + Send + 'a>,
> + Send > + Send
+ Sync, + Sync,
>; >;
pub fn generate_entity_filter_mutation_field<T, N, R>( pub type CreateOneMutationFn<M> = Arc<
dyn for<'a> Fn(
&ResolverContext<'a>,
Arc<dyn AppContextTrait>,
ObjectAccessor<'_>,
) -> Pin<Box<dyn Future<Output = RecorderResult<M>> + Send + 'a>>
+ Send
+ Sync,
>;
pub type CreateBatchMutationFn<M> = Arc<
dyn for<'a> Fn(
&ResolverContext<'a>,
Arc<dyn AppContextTrait>,
Vec<ObjectAccessor<'_>>,
) -> Pin<Box<dyn Future<Output = RecorderResult<Vec<M>>> + Send + 'a>>
+ Send
+ Sync,
>;
pub type UpdateMutationFn<M> = Arc<
dyn for<'a> Fn(
&ResolverContext<'a>,
Arc<dyn AppContextTrait>,
Condition,
ObjectAccessor<'_>,
) -> Pin<Box<dyn Future<Output = RecorderResult<Vec<M>>> + Send + 'a>>
+ Send
+ Sync,
>;
pub type DeleteMutationFn = Arc<
dyn for<'a> Fn(
&ResolverContext<'a>,
Arc<dyn AppContextTrait>,
Condition,
) -> Pin<Box<dyn Future<Output = RecorderResult<u64>> + Send + 'a>>
+ Send
+ Sync,
>;
pub fn generate_entity_default_insert_input_object<T>(
builder_context: &'static BuilderContext,
) -> InputObject
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_input_builder = seaography::EntityInputBuilder {
context: builder_context,
};
entity_input_builder.insert_input_object::<T>()
}
pub fn generate_entity_default_update_input_object<T>(
builder_context: &'static BuilderContext,
) -> InputObject
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_input_builder = seaography::EntityInputBuilder {
context: builder_context,
};
entity_input_builder.update_input_object::<T>()
}
pub fn generate_entity_default_basic_entity_object<T>(
builder_context: &'static BuilderContext,
) -> Object
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_object_builder = seaography::EntityObjectBuilder {
context: builder_context,
};
entity_object_builder.basic_to_object::<T>()
}
pub fn generate_entity_filtered_mutation_field<E, N, R>(
builder_context: &'static BuilderContext, builder_context: &'static BuilderContext,
field_name: N, field_name: N,
type_ref: R, type_ref: R,
mutation_fn: FilterMutationFn, mutation_fn: FilterMutationFn,
) -> Field ) -> Field
where where
T: EntityTrait, E: EntityTrait,
<T as EntityTrait>::Model: Sync, <E as EntityTrait>::Model: Sync,
N: Into<String>, N: Into<String>,
R: Into<TypeRef>, R: Into<TypeRef>,
{ {
let entity_filter_input_builder = FilterInputBuilder { let object_name: String = get_entity_name::<E>(builder_context);
context: builder_context,
};
let entity_object_builder = EntityObjectBuilder {
context: builder_context,
};
let object_name: String = entity_object_builder.type_name::<T>();
let context = builder_context;
let guard = builder_context.guards.entity_guards.get(&object_name); let guard = builder_context.guards.entity_guards.get(&object_name);
@@ -60,7 +155,9 @@ where
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?; let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let filters = ctx.args.get(&context.entity_delete_mutation.filter_field); let filters = ctx.args.get(get_entity_renormalized_filter_field_name());
let filters = get_filter_conditions::<E>(&ctx, builder_context, filters);
let result = mutation_fn(&ctx, app_ctx.clone(), filters) let result = mutation_fn(&ctx, app_ctx.clone(), filters)
.await .await
@@ -70,7 +167,727 @@ where
}) })
}) })
.argument(InputValue::new( .argument(InputValue::new(
&context.entity_delete_mutation.filter_field, get_entity_renormalized_filter_field_name(),
TypeRef::named(entity_filter_input_builder.type_name(&object_name)), TypeRef::named(get_entity_filter_input_type_name::<E>(builder_context)),
)) ))
} }
pub fn generate_entity_create_one_mutation_field<E, ID>(
builder_context: &'static BuilderContext,
input_data_type_ref: Option<ID>,
mutation_fn: CreateOneMutationFn<E::Model>,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
ID: Into<TypeRef>,
{
let guard = builder_context
.guards
.entity_guards
.get(&get_entity_name::<E>(builder_context));
let field_guards = &builder_context.guards.field_guards;
Field::new(
get_entity_create_one_mutation_field_name::<E>(builder_context),
TypeRef::named_nn(get_entity_basic_type_name::<E>(builder_context)),
move |ctx| {
let mutation_fn = mutation_fn.clone();
FieldFuture::new(async move {
let guard_flag = if let Some(guard) = guard {
(*guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = guard_flag {
return Err::<Option<_>, async_graphql::Error>(async_graphql::Error::new(
reason.unwrap_or("Entity guard triggered.".into()),
));
}
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let value_accessor = ctx
.args
.get(get_entity_create_one_mutation_data_field_name(
builder_context,
))
.unwrap();
let input_object = value_accessor.object()?;
for (column, _) in input_object.iter() {
let field_guard = field_guards.get(
&get_entity_and_column_name_from_column_str::<E>(builder_context, column),
);
let field_guard_flag = if let Some(field_guard) = field_guard {
(*field_guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = field_guard_flag {
return match reason {
Some(reason) => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new(reason),
),
None => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new("Field guard triggered."),
),
};
}
}
let result = mutation_fn(&ctx, app_ctx.clone(), input_object)
.await
.map_err(async_graphql::Error::new_with_source)?;
Ok(Some(FieldValue::owned_any(result)))
})
},
)
.argument(InputValue::new(
get_entity_create_one_mutation_data_field_name(builder_context),
input_data_type_ref.map(|t| t.into()).unwrap_or_else(|| {
TypeRef::named_nn(get_entity_insert_data_input_type_name::<E>(builder_context))
}),
))
}
pub fn generate_entity_default_create_one_mutation_fn<T, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> CreateOneMutationFn<T::Model>
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync + IntoActiveModel<A>,
A: ActiveModelTrait<Entity = T> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
Arc::new(move |resolve_context, app_ctx, input_object| {
let entity_input_builder = seaography::EntityInputBuilder {
context: builder_context,
};
let entity_object_builder = seaography::EntityObjectBuilder {
context: builder_context,
};
let active_model = prepare_active_model::<T, A>(
&entity_input_builder,
&entity_object_builder,
&input_object,
resolve_context,
)
.map_err(SeaographyError::AsyncGraphQLError);
Box::pin(async move {
if active_model_hooks {
let transaction = app_ctx.db().begin().await?;
let active_model = active_model?;
let active_model = active_model.before_save(&transaction, true).await?;
let result: T::Model = active_model.insert(&transaction).await?;
let result = A::after_save(result, &transaction, true).await?;
transaction.commit().await?;
Ok(result)
} else {
let db = app_ctx.db();
let active_model = active_model?;
let result: T::Model = active_model.insert(db).await?;
Ok(result)
}
})
})
}
pub fn generate_entity_default_create_one_mutation_field<E, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
generate_entity_create_one_mutation_field::<E, TypeRef>(
builder_context,
None,
generate_entity_default_create_one_mutation_fn::<E, A>(builder_context, active_model_hooks),
)
}
pub fn generate_entity_create_batch_mutation_field<E, ID>(
builder_context: &'static BuilderContext,
input_data_type_ref: Option<ID>,
mutation_fn: CreateBatchMutationFn<E::Model>,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
ID: Into<TypeRef>,
{
let object_name: String = get_entity_name::<E>(builder_context);
let guard = builder_context.guards.entity_guards.get(&object_name);
let field_guards = &builder_context.guards.field_guards;
Field::new(
get_entity_create_batch_mutation_field_name::<E>(builder_context),
TypeRef::named_nn_list_nn(get_entity_basic_type_name::<E>(builder_context)),
move |ctx| {
let mutation_fn = mutation_fn.clone();
FieldFuture::new(async move {
let guard_flag = if let Some(guard) = guard {
(*guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = guard_flag {
return match reason {
Some(reason) => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new(reason),
),
None => Err::<Option<_>, async_graphql::Error>(async_graphql::Error::new(
"Entity guard triggered.",
)),
};
}
let mut input_objects: Vec<ObjectAccessor<'_>> = vec![];
let list = ctx
.args
.get(get_entity_create_batch_mutation_data_field_name(
builder_context,
))
.unwrap()
.list()?;
for input in list.iter() {
let input_object = input.object()?;
for (column, _) in input_object.iter() {
let field_guard =
field_guards.get(&get_entity_and_column_name_from_column_str::<E>(
builder_context,
column,
));
let field_guard_flag = if let Some(field_guard) = field_guard {
(*field_guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = field_guard_flag {
return match reason {
Some(reason) => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new(reason),
),
None => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new("Field guard triggered."),
),
};
}
}
input_objects.push(input_object);
}
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let results = mutation_fn(&ctx, app_ctx.clone(), input_objects)
.await
.map_err(async_graphql::Error::new_with_source)?;
Ok(Some(FieldValue::list(
results.into_iter().map(FieldValue::owned_any),
)))
})
},
)
.argument(InputValue::new(
get_entity_create_batch_mutation_data_field_name(builder_context),
input_data_type_ref.map(|t| t.into()).unwrap_or_else(|| {
TypeRef::named_nn_list_nn(get_entity_insert_data_input_type_name::<E>(builder_context))
}),
))
}
pub fn generate_entity_default_create_batch_mutation_fn<E, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> CreateBatchMutationFn<E::Model>
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
Arc::new(move |resolve_context, app_ctx, input_objects| {
let entity_input_builder = seaography::EntityInputBuilder {
context: builder_context,
};
let entity_object_builder = seaography::EntityObjectBuilder {
context: builder_context,
};
let active_models = input_objects
.into_iter()
.map(|input_object| {
prepare_active_model::<E, A>(
&entity_input_builder,
&entity_object_builder,
&input_object,
resolve_context,
)
})
.collect::<Result<Vec<_>, _>>()
.map_err(SeaographyError::AsyncGraphQLError);
Box::pin(async move {
if active_model_hooks {
let transaction = app_ctx.db().begin().await?;
let mut before_save_models = vec![];
for active_model in active_models? {
let before_save_model = active_model.before_save(&transaction, false).await?;
before_save_models.push(before_save_model);
}
let models: Vec<E::Model> = E::insert_many(before_save_models)
.exec_with_returning_many(&transaction)
.await?;
let mut result = vec![];
for model in models {
let after_save_model = A::after_save(model, &transaction, false).await?;
result.push(after_save_model);
}
transaction.commit().await?;
Ok(result)
} else {
let db = app_ctx.db();
let active_models = active_models?;
let results: Vec<E::Model> = E::insert_many(active_models)
.exec_with_returning_many(db)
.await?;
Ok(results)
}
})
})
}
pub fn generate_entity_default_create_batch_mutation_field<E, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
generate_entity_create_batch_mutation_field::<E, TypeRef>(
builder_context,
None,
generate_entity_default_create_batch_mutation_fn::<E, A>(
builder_context,
active_model_hooks,
),
)
}
pub fn generate_entity_update_mutation_field<E, I>(
builder_context: &'static BuilderContext,
input_data_type_ref: Option<I>,
mutation_fn: UpdateMutationFn<E::Model>,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
I: Into<TypeRef>,
{
let guard = builder_context
.guards
.entity_guards
.get(&get_entity_name::<E>(builder_context));
let field_guards = &builder_context.guards.field_guards;
Field::new(
get_entity_update_mutation_field_name::<E>(builder_context),
TypeRef::named_nn_list_nn(get_entity_basic_type_name::<E>(builder_context)),
move |ctx| {
let mutation_fn = mutation_fn.clone();
FieldFuture::new(async move {
let guard_flag = if let Some(guard) = guard {
(*guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = guard_flag {
return match reason {
Some(reason) => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new(reason),
),
None => Err::<Option<_>, async_graphql::Error>(async_graphql::Error::new(
"Entity guard triggered.",
)),
};
}
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let filters = ctx.args.get(get_entity_update_mutation_filter_field_name(
builder_context,
));
let filter_condition = get_filter_conditions::<E>(&ctx, builder_context, filters);
let value_accessor = ctx
.args
.get(get_entity_update_mutation_data_field_name(builder_context))
.unwrap();
let input_object = value_accessor.object()?;
for (column, _) in input_object.iter() {
let field_guard = field_guards.get(
&get_entity_and_column_name_from_column_str::<E>(builder_context, column),
);
let field_guard_flag = if let Some(field_guard) = field_guard {
(*field_guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = field_guard_flag {
return match reason {
Some(reason) => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new(reason),
),
None => Err::<Option<_>, async_graphql::Error>(
async_graphql::Error::new("Field guard triggered."),
),
};
}
}
let result = mutation_fn(&ctx, app_ctx.clone(), filter_condition, input_object)
.await
.map_err(async_graphql::Error::new_with_source)?;
Ok(Some(FieldValue::list(
result.into_iter().map(FieldValue::owned_any),
)))
})
},
)
.argument(InputValue::new(
get_entity_update_mutation_data_field_name(builder_context),
input_data_type_ref.map(|t| t.into()).unwrap_or_else(|| {
TypeRef::named_nn(get_entity_update_data_input_type_name::<E>(builder_context))
}),
))
.argument(InputValue::new(
get_entity_update_mutation_filter_field_name(builder_context),
TypeRef::named(get_entity_filter_input_type_name::<E>(builder_context)),
))
}
pub fn generate_entity_default_update_mutation_fn<T, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> UpdateMutationFn<T::Model>
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync + IntoActiveModel<A>,
A: ActiveModelTrait<Entity = T> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
Arc::new(
move |resolve_context, app_ctx, filter_condition, input_object| {
let entity_input_builder = seaography::EntityInputBuilder {
context: builder_context,
};
let entity_object_builder = seaography::EntityObjectBuilder {
context: builder_context,
};
let active_model = prepare_active_model::<T, A>(
&entity_input_builder,
&entity_object_builder,
&input_object,
resolve_context,
)
.map_err(SeaographyError::AsyncGraphQLError);
Box::pin(async move {
if active_model_hooks {
let transaction = app_ctx.db().begin().await?;
let active_model = active_model?;
let active_model = active_model.before_save(&transaction, false).await?;
let models = T::update_many()
.set(active_model)
.filter(filter_condition.clone())
.exec_with_returning(&transaction)
.await?;
let mut result = vec![];
for model in models {
result.push(A::after_save(model, &transaction, false).await?);
}
transaction.commit().await?;
Ok(result)
} else {
let db = app_ctx.db();
let active_model = active_model?;
let result = T::update_many()
.set(active_model)
.filter(filter_condition.clone())
.exec_with_returning(db)
.await?;
Ok(result)
}
})
},
)
}
pub fn generate_entity_default_update_mutation_field<E, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
generate_entity_update_mutation_field::<E, TypeRef>(
builder_context,
None,
generate_entity_default_update_mutation_fn::<E, A>(builder_context, active_model_hooks),
)
}
pub fn generate_entity_delete_mutation_field<E>(
builder_context: &'static BuilderContext,
mutation_fn: DeleteMutationFn,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
{
let object_name: String = get_entity_name::<E>(builder_context);
let guard = builder_context.guards.entity_guards.get(&object_name);
Field::new(
get_entity_delete_mutation_field_name::<E>(builder_context),
TypeRef::named_nn(TypeRef::INT),
move |ctx| {
let mutation_fn = mutation_fn.clone();
FieldFuture::new(async move {
let guard_flag = if let Some(guard) = guard {
(*guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = guard_flag {
return Err::<Option<_>, async_graphql::Error>(async_graphql::Error::new(
reason.unwrap_or("Entity guard triggered.".into()),
));
}
let filters = ctx.args.get(get_entity_delete_mutation_filter_field_name(
builder_context,
));
let filter_condition = get_filter_conditions::<E>(&ctx, builder_context, filters);
let app_ctx = ctx.data::<Arc<dyn AppContextTrait>>()?;
let res = mutation_fn(&ctx, app_ctx.clone(), filter_condition)
.await
.map_err(async_graphql::Error::new_with_source)?;
Ok(Some(async_graphql::Value::from(res)))
})
},
)
.argument(InputValue::new(
get_entity_delete_mutation_filter_field_name(builder_context),
TypeRef::named(get_entity_filter_input_type_name::<E>(builder_context)),
))
}
pub fn generate_entity_default_delete_mutation_fn<E, A>(
_builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> DeleteMutationFn
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
Arc::new(move |_resolve_context, app_ctx, filter_condition| {
Box::pin(async move {
if active_model_hooks {
let transaction = app_ctx.db().begin().await?;
let models: Vec<E::Model> = E::find()
.filter(filter_condition.clone())
.all(&transaction)
.await?;
let mut active_models: Vec<A> = vec![];
for model in models {
let active_model = model.into_active_model();
active_models.push(active_model.before_delete(&transaction).await?);
}
let result = E::delete_many()
.filter(filter_condition)
.exec(&transaction)
.await?;
for active_model in active_models {
active_model.after_delete(&transaction).await?;
}
transaction.commit().await?;
Ok(result.rows_affected)
} else {
let db = app_ctx.db();
let result = E::delete_many().filter(filter_condition).exec(db).await?;
Ok(result.rows_affected)
}
})
})
}
pub fn generate_entity_default_delete_mutation_field<E, A>(
builder_context: &'static BuilderContext,
active_model_hooks: bool,
) -> Field
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
<E as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
generate_entity_delete_mutation_field::<E>(
builder_context,
generate_entity_default_delete_mutation_fn::<E, A>(builder_context, active_model_hooks),
)
}
pub fn register_entity_default_mutations<E, A>(
mut builder: SeaographyBuilder,
active_model_hooks: bool,
) -> SeaographyBuilder
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync + IntoActiveModel<A>,
A: ActiveModelTrait<Entity = E> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
{
builder
.outputs
.push(generate_entity_default_basic_entity_object::<E>(
builder.context,
));
builder.inputs.extend([
generate_entity_default_insert_input_object::<E>(builder.context),
generate_entity_default_update_input_object::<E>(builder.context),
]);
builder.mutations.extend([
generate_entity_default_create_one_mutation_field::<E, A>(
builder.context,
active_model_hooks,
),
generate_entity_default_create_batch_mutation_field::<E, A>(
builder.context,
active_model_hooks,
),
generate_entity_default_update_mutation_field::<E, A>(builder.context, active_model_hooks),
generate_entity_default_delete_mutation_field::<E, A>(builder.context, active_model_hooks),
]);
builder
}
pub(crate) fn register_entity_default_readonly_impl<T, RE, I>(
mut builder: SeaographyBuilder,
entity: T,
) -> SeaographyBuilder
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
RE: sea_orm::Iterable<Iterator = I> + RelationBuilder,
I: Iterator<Item = RE> + Clone + DoubleEndedIterator + ExactSizeIterator + FusedIterator,
{
builder.register_entity::<T>(
<RE as sea_orm::Iterable>::iter()
.map(|rel| RelationBuilder::get_relation(&rel, builder.context))
.collect(),
);
builder = builder.register_entity_dataloader_one_to_one(entity, tokio::spawn);
builder = builder.register_entity_dataloader_one_to_many(entity, tokio::spawn);
builder
}
pub(crate) fn register_entity_default_writable_impl<T, RE, A, I>(
mut builder: SeaographyBuilder,
entity: T,
active_model_hooks: bool,
) -> SeaographyBuilder
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync + IntoActiveModel<A>,
A: ActiveModelTrait<Entity = T> + sea_orm::ActiveModelBehavior + std::marker::Send + 'static,
RE: sea_orm::Iterable<Iterator = I> + RelationBuilder,
I: Iterator<Item = RE> + Clone + DoubleEndedIterator + ExactSizeIterator + FusedIterator,
{
builder = register_entity_default_readonly_impl::<T, RE, I>(builder, entity);
builder = register_entity_default_mutations::<T, A>(builder, active_model_hooks);
builder
}
macro_rules! register_entity_default_readonly {
($builder:expr, $module_path:ident) => {
$crate::graphql::infra::custom::register_entity_default_readonly_impl::<
$module_path::Entity,
$module_path::RelatedEntity,
_,
>($builder, $module_path::Entity)
};
}
macro_rules! register_entity_default_writable {
($builder:expr, $module_path:ident, $active_model_hooks:expr) => {
$crate::graphql::infra::custom::register_entity_default_writable_impl::<
$module_path::Entity,
$module_path::RelatedEntity,
$module_path::ActiveModel,
_,
>($builder, $module_path::Entity, $active_model_hooks)
};
}
pub(crate) use register_entity_default_readonly;
pub(crate) use register_entity_default_writable;

View File

@@ -17,7 +17,7 @@ use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use crate::{ use crate::{
errors::RecorderResult, graphql::infra::util::get_entity_column_key, errors::RecorderResult, graphql::infra::name::get_entity_and_column_name,
utils::json::convert_json_keys, utils::json::convert_json_keys,
}; };
@@ -946,9 +946,8 @@ where
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_column_key = get_entity_column_key::<T>(context, column);
context.filter_types.overwrites.insert( context.filter_types.overwrites.insert(
entity_column_key.clone(), get_entity_and_column_name::<T>(context, column),
Some(FilterType::Custom(JSONB_FILTER_NAME.to_string())), Some(FilterType::Custom(JSONB_FILTER_NAME.to_string())),
); );
} }
@@ -959,20 +958,20 @@ where
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
S: DeserializeOwned + Serialize, S: DeserializeOwned + Serialize,
{ {
let entity_column_key = get_entity_column_key::<T>(context, column); let entity_column_name = get_entity_and_column_name::<T>(context, column);
context.types.input_conversions.insert( context.types.input_conversions.insert(
entity_column_key.clone(), entity_column_name.clone(),
Box::new(move |_resolve_context, accessor| { Box::new(move |_resolve_context, accessor| {
let deserialized = accessor.deserialize::<S>().map_err(|err| { let deserialized = accessor.deserialize::<S>().map_err(|err| {
SeaographyError::TypeConversionError( SeaographyError::TypeConversionError(
err.message, err.message,
format!("Json - {entity_column_key}"), format!("Json - {entity_column_name}"),
) )
})?; })?;
let json_value = serde_json::to_value(deserialized).map_err(|err| { let json_value = serde_json::to_value(deserialized).map_err(|err| {
SeaographyError::TypeConversionError( SeaographyError::TypeConversionError(
err.to_string(), err.to_string(),
format!("Json - {entity_column_key}"), format!("Json - {entity_column_name}"),
) )
})?; })?;
Ok(sea_orm::Value::Json(Some(Box::new(json_value)))) Ok(sea_orm::Value::Json(Some(Box::new(json_value))))
@@ -980,20 +979,21 @@ where
); );
} }
pub fn convert_jsonb_output_case_for_entity<T>(context: &mut BuilderContext, column: &T::Column) pub fn convert_jsonb_output_case_for_entity<T>(
where context: &mut BuilderContext,
column: &T::Column,
case: Case<'static>,
) where
T: EntityTrait, T: EntityTrait,
<T as EntityTrait>::Model: Sync, <T as EntityTrait>::Model: Sync,
{ {
let entity_column_key = get_entity_column_key::<T>(context, column); let entity_column_key = get_entity_and_column_name::<T>(context, column);
context.types.output_conversions.insert( context.types.output_conversions.insert(
entity_column_key.clone(), entity_column_key.clone(),
Box::new(move |value| { Box::new(move |value| {
if let sea_orm::Value::Json(Some(json)) = value { if let sea_orm::Value::Json(Some(json)) = value {
let result = async_graphql::Value::from_json(convert_json_keys( let result =
json.as_ref().clone(), async_graphql::Value::from_json(convert_json_keys(json.as_ref().clone(), case))
Case::Camel,
))
.map_err(|err| { .map_err(|err| {
SeaographyError::TypeConversionError( SeaographyError::TypeConversionError(
err.to_string(), err.to_string(),

View File

@@ -1,4 +1,4 @@
pub mod crypto; pub mod crypto;
pub mod custom; pub mod custom;
pub mod json; pub mod json;
pub mod util; pub mod name;

View File

@@ -0,0 +1,203 @@
use std::fmt::Display;
use sea_orm::{EntityName, EntityTrait, IdenStatic};
use seaography::BuilderContext;
pub fn get_entity_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let t = T::default();
let name = <T as EntityName>::table_name(&t);
context.entity_object.type_name.as_ref()(name)
}
pub fn get_column_name<T>(context: &BuilderContext, column: &T::Column) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
context.entity_object.column_name.as_ref()(&entity_name, column.as_str())
}
pub fn get_entity_and_column_name<T>(context: &BuilderContext, column: &T::Column) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
let column_name = get_column_name::<T>(context, column);
format!("{entity_name}.{column_name}")
}
pub fn get_entity_and_column_name_from_column_str<T>(
context: &BuilderContext,
column_str: &str,
) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
format!("{entity_name}.{column_str}")
}
pub fn get_entity_basic_type_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let t = T::default();
let name = <T as EntityName>::table_name(&t);
format!(
"{}{}",
context.entity_object.type_name.as_ref()(name),
context.entity_object.basic_type_suffix
)
}
pub fn get_entity_query_field_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
context.entity_query_field.type_name.as_ref()(&entity_name)
}
pub fn get_entity_filter_input_type_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
context.filter_input.type_name.as_ref()(&entity_name)
}
pub fn get_entity_insert_data_input_type_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
format!("{entity_name}{}", context.entity_input.insert_suffix)
}
pub fn get_entity_update_data_input_type_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_name::<T>(context);
format!("{entity_name}{}", context.entity_input.update_suffix)
}
pub fn get_entity_create_one_mutation_field_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let query_field_name = get_entity_query_field_name::<T>(context);
format!(
"{}{}",
query_field_name, context.entity_create_one_mutation.mutation_suffix
)
}
pub fn get_entity_create_batch_mutation_field_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let query_field_name = get_entity_query_field_name::<T>(context);
format!(
"{}{}",
query_field_name, context.entity_create_batch_mutation.mutation_suffix
)
}
pub fn get_entity_delete_mutation_field_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let query_field_name = get_entity_query_field_name::<T>(context);
format!(
"{}{}",
query_field_name, context.entity_delete_mutation.mutation_suffix
)
}
pub fn get_entity_update_mutation_field_name<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let query_field_name = get_entity_query_field_name::<T>(context);
format!(
"{}{}",
query_field_name, context.entity_update_mutation.mutation_suffix
)
}
pub fn get_entity_custom_mutation_field_name<T>(
context: &BuilderContext,
mutation_suffix: impl Display,
) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let query_field_name = get_entity_query_field_name::<T>(context);
format!("{query_field_name}{mutation_suffix}")
}
pub fn get_entity_renormalized_filter_field_name() -> &'static str {
"filter"
}
pub fn get_entity_query_filter_field_name(context: &BuilderContext) -> &str {
&context.entity_query_field.filters
}
pub fn get_entity_update_mutation_filter_field_name(context: &BuilderContext) -> &str {
&context.entity_update_mutation.filter_field
}
pub fn get_entity_delete_mutation_filter_field_name(context: &BuilderContext) -> &str {
&context.entity_delete_mutation.filter_field
}
pub fn renormalize_filter_field_names_to_schema_context(context: &mut BuilderContext) {
let renormalized_filter_field_name = get_entity_renormalized_filter_field_name();
context.entity_query_field.filters = renormalized_filter_field_name.to_string();
context.entity_update_mutation.filter_field = renormalized_filter_field_name.to_string();
context.entity_delete_mutation.filter_field = renormalized_filter_field_name.to_string();
}
pub fn get_entity_renormalized_data_field_name() -> &'static str {
"data"
}
pub fn get_entity_create_one_mutation_data_field_name(context: &BuilderContext) -> &str {
&context.entity_create_one_mutation.data_field
}
pub fn get_entity_create_batch_mutation_data_field_name(context: &BuilderContext) -> &str {
&context.entity_create_batch_mutation.data_field
}
pub fn get_entity_update_mutation_data_field_name(context: &BuilderContext) -> &str {
&context.entity_update_mutation.data_field
}
pub fn renormalize_data_field_names_to_schema_context(context: &mut BuilderContext) {
let renormalized_data_field_name = get_entity_renormalized_data_field_name();
context.entity_create_one_mutation.data_field = renormalized_data_field_name.to_string();
context.entity_create_batch_mutation.data_field = renormalized_data_field_name.to_string();
context.entity_update_mutation.data_field = renormalized_data_field_name.to_string();
}

View File

@@ -1,30 +0,0 @@
use sea_orm::{EntityName, EntityTrait, IdenStatic};
use seaography::BuilderContext;
pub fn get_entity_key<T>(context: &BuilderContext) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
context.entity_object.type_name.as_ref()(<T as EntityName>::table_name(&T::default()))
}
pub fn get_column_key<T>(context: &BuilderContext, column: &T::Column) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_key::<T>(context);
context.entity_object.column_name.as_ref()(&entity_name, column.as_str())
}
pub fn get_entity_column_key<T>(context: &BuilderContext, column: &T::Column) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = get_entity_key::<T>(context);
let column_name = get_column_key::<T>(context, column);
format!("{}.{}", &entity_name, &column_name)
}

View File

@@ -12,6 +12,7 @@ use crate::{
credential_3rd::{ credential_3rd::{
register_credential3rd_to_schema_builder, register_credential3rd_to_schema_context, register_credential3rd_to_schema_builder, register_credential3rd_to_schema_context,
}, },
cron::{register_cron_to_schema_builder, register_cron_to_schema_context},
downloaders::{ downloaders::{
register_downloaders_to_schema_builder, register_downloaders_to_schema_context, register_downloaders_to_schema_builder, register_downloaders_to_schema_context,
}, },
@@ -39,7 +40,13 @@ use crate::{
register_subscriptions_to_schema_builder, register_subscriptions_to_schema_context, register_subscriptions_to_schema_builder, register_subscriptions_to_schema_context,
}, },
}, },
infra::json::register_jsonb_input_filter_to_schema_builder, infra::{
json::register_jsonb_input_filter_to_schema_builder,
name::{
renormalize_data_field_names_to_schema_context,
renormalize_filter_field_names_to_schema_context,
},
},
}, },
}; };
@@ -55,6 +62,9 @@ pub fn build_schema(
let context = CONTEXT.get_or_init(|| { let context = CONTEXT.get_or_init(|| {
let mut context = BuilderContext::default(); let mut context = BuilderContext::default();
renormalize_filter_field_names_to_schema_context(&mut context);
renormalize_data_field_names_to_schema_context(&mut context);
{ {
// domains // domains
register_feeds_to_schema_context(&mut context); register_feeds_to_schema_context(&mut context);
@@ -68,6 +78,7 @@ pub fn build_schema(
register_subscription_bangumi_to_schema_context(&mut context); register_subscription_bangumi_to_schema_context(&mut context);
register_subscription_episode_to_schema_context(&mut context); register_subscription_episode_to_schema_context(&mut context);
register_bangumi_to_schema_context(&mut context); register_bangumi_to_schema_context(&mut context);
register_cron_to_schema_context(&mut context);
} }
context context
}); });
@@ -91,6 +102,7 @@ pub fn build_schema(
builder = register_credential3rd_to_schema_builder(builder); builder = register_credential3rd_to_schema_builder(builder);
builder = register_subscriber_tasks_to_schema_builder(builder); builder = register_subscriber_tasks_to_schema_builder(builder);
builder = register_bangumi_to_schema_builder(builder); builder = register_bangumi_to_schema_builder(builder);
builder = register_cron_to_schema_builder(builder);
} }
let schema = builder.schema_builder(); let schema = builder.schema_builder();

View File

@@ -7,11 +7,11 @@
async_fn_traits, async_fn_traits,
error_generic_member_access, error_generic_member_access,
associated_type_defaults, associated_type_defaults,
let_chains let_chains,
impl_trait_in_fn_trait_return
)] )]
#![allow(clippy::enum_variant_names)] #![allow(clippy::enum_variant_names)]
pub use downloader; pub use downloader;
pub mod app; pub mod app;
pub mod auth; pub mod auth;
pub mod cache; pub mod cache;

View File

@@ -171,6 +171,27 @@ pub enum Feeds {
SubscriptionId, SubscriptionId,
} }
#[derive(DeriveIden)]
pub enum Cron {
Table,
Id,
SubscriberId,
SubscriptionId,
CronExpr,
NextRun,
LastRun,
LastError,
Enabled,
LockedBy,
LockedAt,
TimeoutMs,
Attempts,
MaxAttempts,
Priority,
Status,
SubscriberTask,
}
macro_rules! create_postgres_enum_for_active_enum { macro_rules! create_postgres_enum_for_active_enum {
($manager: expr, $active_enum: expr, $($enum_value:expr),+) => { ($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
{ {

View File

@@ -52,8 +52,7 @@ impl MigrationTrait for Migration {
subscriptions::SubscriptionCategoryEnum, subscriptions::SubscriptionCategoryEnum,
subscriptions::SubscriptionCategory::MikanSubscriber, subscriptions::SubscriptionCategory::MikanSubscriber,
subscriptions::SubscriptionCategory::MikanBangumi, subscriptions::SubscriptionCategory::MikanBangumi,
subscriptions::SubscriptionCategory::MikanSeason, subscriptions::SubscriptionCategory::MikanSeason
subscriptions::SubscriptionCategory::Manual
) )
.await?; .await?;

View File

@@ -0,0 +1,62 @@
use async_trait::async_trait;
use sea_orm_migration::prelude::*;
use crate::task::SUBSCRIBER_TASK_APALIS_NAME;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE VIEW subscriber_tasks AS
SELECT
job,
job_type,
status,
(job ->> 'subscriber_id')::integer AS subscriber_id,
job ->> 'task_type' AS task_type,
id,
attempts,
max_attempts,
run_at,
last_error,
lock_at,
lock_by,
done_at,
priority,
(job ->> 'subscription_id')::integer AS subscription_id
FROM apalis.jobs
WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}'
AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")')
AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#,
))
.await?;
db.execute_unprepared(&format!(
r#"CREATE INDEX IF NOT EXISTS idx_apalis_jobs_subscription_id
ON apalis.jobs (((job -> 'subscription_id')::integer))
WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}'
AND jsonb_path_exists(job, '$.subscription_id ? (@.type() == "number")')
AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"DROP INDEX IF EXISTS idx_apalis_jobs_subscription_id
ON apalis.jobs"#,
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,248 @@
use async_trait::async_trait;
use sea_orm::ActiveEnum;
use sea_orm_migration::{prelude::*, schema::*};
use crate::{
migrations::defs::{
Cron, CustomSchemaManagerExt, GeneralIds, Subscribers, Subscriptions, table_auto_z,
},
models::cron::{
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT, CronStatus, CronStatusEnum,
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,
},
};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
create_postgres_enum_for_active_enum!(
manager,
CronStatusEnum,
CronStatus::Pending,
CronStatus::Running,
CronStatus::Completed,
CronStatus::Failed
)
.await?;
manager
.create_table(
table_auto_z(Cron::Table)
.col(pk_auto(Cron::Id))
.col(string(Cron::CronExpr))
.col(integer_null(Cron::SubscriberId))
.col(integer_null(Cron::SubscriptionId))
.col(timestamp_with_time_zone_null(Cron::NextRun))
.col(timestamp_with_time_zone_null(Cron::LastRun))
.col(string_null(Cron::LastError))
.col(boolean(Cron::Enabled).default(true))
.col(string_null(Cron::LockedBy))
.col(timestamp_with_time_zone_null(Cron::LockedAt))
.col(integer_null(Cron::TimeoutMs))
.col(integer(Cron::Attempts))
.col(integer(Cron::MaxAttempts))
.col(integer(Cron::Priority))
.col(enumeration(
Cron::Status,
CronStatusEnum,
CronStatus::iden_values(),
))
.col(json_binary_null(Cron::SubscriberTask))
.foreign_key(
ForeignKey::create()
.name("fk_cron_subscriber_id")
.from(Cron::Table, Cron::SubscriberId)
.to(Subscribers::Table, Subscribers::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Restrict),
)
.foreign_key(
ForeignKey::create()
.name("fk_cron_subscription_id")
.from(Cron::Table, Cron::SubscriptionId)
.to(Subscriptions::Table, Subscriptions::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Restrict),
)
.to_owned(),
)
.await?;
manager
.create_postgres_auto_update_ts_trigger_for_col(Cron::Table, GeneralIds::UpdatedAt)
.await?;
manager
.create_index(
IndexCreateStatement::new()
.if_not_exists()
.name("idx_cron_next_run")
.table(Cron::Table)
.col(Cron::NextRun)
.to_owned(),
)
.await?;
let db = manager.get_connection();
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE FUNCTION {SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}() RETURNS trigger AS $$
BEGIN
IF jsonb_path_exists(NEW.{subscriber_task}, '$.subscriber_id ? (@.type() == "number")') THEN
NEW.{subscriber_id} = (NEW.{subscriber_task} ->> 'subscriber_id')::integer;
END IF;
IF jsonb_path_exists(NEW.{subscriber_task}, '$.subscription_id ? (@.type() == "number")') THEN
NEW.{subscription_id} = (NEW.{subscriber_task} ->> 'subscription_id')::integer;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;"#,
subscriber_task = &Cron::SubscriberTask.to_string(),
subscriber_id = &Cron::SubscriberId.to_string(),
subscription_id = &Cron::SubscriptionId.to_string(),
)).await?;
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE TRIGGER {SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME}
BEFORE INSERT OR UPDATE ON {table}
FOR EACH ROW
EXECUTE FUNCTION {SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}();"#,
table = &Cron::Table.to_string(),
))
.await?;
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE FUNCTION {NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME}() RETURNS trigger AS $$
BEGIN
-- Check if the cron is due to run
IF NEW.{next_run} IS NOT NULL
AND NEW.{next_run} <= CURRENT_TIMESTAMP
AND NEW.{enabled} = true
AND NEW.{status} = '{pending}'
AND NEW.{attempts} < NEW.{max_attempts}
-- Check if not locked or lock timeout
AND (
NEW.{locked_at} IS NULL
OR (
NEW.{timeout_ms} IS NOT NULL
AND (NEW.{locked_at} + NEW.{timeout_ms} * INTERVAL '1 millisecond') <= CURRENT_TIMESTAMP
)
)
-- Make sure the cron is a new due event, not a repeat event
AND (
OLD.{next_run} IS NULL
OR OLD.{next_run} > CURRENT_TIMESTAMP
OR OLD.{enabled} = false
OR OLD.{status} != '{pending}'
OR OLD.{attempts} != NEW.{attempts}
)
THEN
PERFORM pg_notify('{CRON_DUE_EVENT}', row_to_json(NEW)::text);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;"#,
next_run = &Cron::NextRun.to_string(),
enabled = &Cron::Enabled.to_string(),
locked_at = &Cron::LockedAt.to_string(),
timeout_ms = &Cron::TimeoutMs.to_string(),
status = &Cron::Status.to_string(),
pending = &CronStatus::Pending.to_value(),
attempts = &Cron::Attempts.to_string(),
max_attempts = &Cron::MaxAttempts.to_string(),
))
.await?;
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE TRIGGER {NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME}
AFTER INSERT OR UPDATE ON {table}
FOR EACH ROW
EXECUTE FUNCTION {NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME}();"#,
table = &Cron::Table.to_string(),
))
.await?;
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE FUNCTION {CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME}() RETURNS INTEGER AS $$
DECLARE
cron_record RECORD;
notification_count INTEGER := 0;
BEGIN
FOR cron_record IN
SELECT * FROM {table}
WHERE {next_run} IS NOT NULL
AND {next_run} <= CURRENT_TIMESTAMP
AND {enabled} = true
AND {status} = '{pending}'
AND {attempts} < {max_attempts}
AND (
{locked_at} IS NULL
OR (
{timeout_ms} IS NOT NULL
AND {locked_at} + {timeout_ms} * INTERVAL '1 millisecond' <= CURRENT_TIMESTAMP
)
)
ORDER BY {priority} ASC, {next_run} ASC
FOR UPDATE SKIP LOCKED
LOOP
PERFORM pg_notify('{CRON_DUE_EVENT}', row_to_json(cron_record)::text);
notification_count := notification_count + 1;
END LOOP;
RETURN notification_count;
END;
$$ LANGUAGE plpgsql;"#,
table = &Cron::Table.to_string(),
next_run = &Cron::NextRun.to_string(),
enabled = &Cron::Enabled.to_string(),
status = &Cron::Status.to_string(),
pending = &CronStatus::Pending.to_value(),
locked_at = &Cron::LockedAt.to_string(),
timeout_ms = &Cron::TimeoutMs.to_string(),
priority = &Cron::Priority.to_string(),
attempts = &Cron::Attempts.to_string(),
max_attempts = &Cron::MaxAttempts.to_string(),
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(&format!(
r#"DROP TRIGGER IF EXISTS {NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME} ON {table};"#,
table = &Cron::Table.to_string(),
))
.await?;
db.execute_unprepared(&format!(
r#"DROP FUNCTION IF EXISTS {NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME}();"#,
))
.await?;
db.execute_unprepared(&format!(
r#"DROP FUNCTION IF EXISTS {CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME}();"#,
))
.await?;
manager
.drop_table(
TableDropStatement::new()
.if_exists()
.table(Cron::Table)
.to_owned(),
)
.await?;
manager
.drop_postgres_enum_for_active_enum(CronStatusEnum)
.await?;
Ok(())
}
}

View File

@@ -10,6 +10,8 @@ pub mod m20250501_021523_credential_3rd;
pub mod m20250520_021135_subscriber_tasks; pub mod m20250520_021135_subscriber_tasks;
pub mod m20250622_015618_feeds; pub mod m20250622_015618_feeds;
pub mod m20250622_020819_bangumi_and_episode_type; pub mod m20250622_020819_bangumi_and_episode_type;
pub mod m20250625_060701_add_subscription_id_to_subscriber_tasks;
pub mod m20250629_065628_add_cron;
pub struct Migrator; pub struct Migrator;
@@ -24,6 +26,8 @@ impl MigratorTrait for Migrator {
Box::new(m20250520_021135_subscriber_tasks::Migration), Box::new(m20250520_021135_subscriber_tasks::Migration),
Box::new(m20250622_015618_feeds::Migration), Box::new(m20250622_015618_feeds::Migration),
Box::new(m20250622_020819_bangumi_and_episode_type::Migration), Box::new(m20250622_020819_bangumi_and_episode_type::Migration),
Box::new(m20250625_060701_add_subscription_id_to_subscriber_tasks::Migration),
Box::new(m20250629_065628_add_cron::Migration),
] ]
} }
} }

View File

@@ -63,7 +63,11 @@ impl Model {
.filter(Column::Pid.eq(pid)) .filter(Column::Pid.eq(pid))
.one(db) .one(db)
.await? .await?
.ok_or_else(|| RecorderError::from_db_record_not_found("auth::find_by_pid"))?; .ok_or_else(|| {
RecorderError::from_entity_not_found_detail::<Entity, _>(format!(
"pid {pid} not found"
))
})?;
Ok(subscriber_auth) Ok(subscriber_auth)
} }

View File

@@ -0,0 +1,10 @@
pub const CRON_DUE_EVENT: &str = "cron_due";
pub const CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME: &str = "check_and_trigger_due_crons";
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";

View File

@@ -0,0 +1,367 @@
mod core;
mod registry;
pub use core::{
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT,
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;
use chrono::{DateTime, Utc};
use croner::Cron;
use sea_orm::{
ActiveValue::{self, Set},
Condition, DeriveActiveEnum, DeriveDisplay, DeriveEntityModel, EnumIter, QuerySelect,
Statement, TransactionTrait,
entity::prelude::*,
sea_query::{ExprTrait, LockBehavior, LockType},
sqlx::postgres::PgNotification,
};
use serde::{Deserialize, Serialize};
use crate::{
app::AppContextTrait, errors::RecorderResult, models::subscriber_tasks,
task::SubscriberTaskTrait,
};
#[derive(
Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "cron_status")]
#[serde(rename_all = "snake_case")]
pub enum CronStatus {
#[sea_orm(string_value = "pending")]
Pending,
#[sea_orm(string_value = "running")]
Running,
#[sea_orm(string_value = "completed")]
Completed,
#[sea_orm(string_value = "failed")]
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "cron")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTimeUtc,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTimeUtc,
#[sea_orm(primary_key)]
pub id: i32,
pub subscriber_id: Option<i32>,
pub subscription_id: Option<i32>,
pub cron_expr: String,
pub next_run: Option<DateTimeUtc>,
pub last_run: Option<DateTimeUtc>,
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,
#[sea_orm(default_expr = "1")]
pub max_attempts: i32,
#[sea_orm(default_expr = "0")]
pub priority: i32,
pub status: CronStatus,
#[sea_orm(default_expr = "true")]
pub enabled: bool,
pub subscriber_task: Option<subscriber_tasks::SubscriberTask>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Subscriber,
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Subscription,
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(mut self, _db: &C, _insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if let ActiveValue::Set(ref cron_expr) = self.cron_expr
&& matches!(
self.next_run,
ActiveValue::NotSet | ActiveValue::Unchanged(_)
)
{
let next_run =
Model::calculate_next_run(cron_expr).map_err(|e| DbErr::Custom(e.to_string()))?;
self.next_run = Set(Some(next_run));
}
if let ActiveValue::Set(Some(subscriber_id)) = self.subscriber_id {
if let ActiveValue::Set(Some(ref subscriber_task)) = self.subscriber_task {
if subscriber_task.get_subscriber_id() != subscriber_id {
return Err(DbErr::Custom(
"Subscriber task subscriber_id does not match cron subscriber_id"
.to_string(),
));
}
} else {
return Err(DbErr::Custom(
"Cron subscriber_id is set but subscriber_task is not set".to_string(),
));
}
}
Ok(self)
}
}
impl Model {
pub async fn handle_cron_notification(
ctx: &dyn AppContextTrait,
notification: PgNotification,
worker_id: &str,
retry_duration: chrono::Duration,
) -> RecorderResult<()> {
let payload: Self = serde_json::from_str(notification.payload())?;
let cron_id = payload.id;
tracing::debug!("Cron notification received for cron {cron_id} and worker {worker_id}");
match Self::try_acquire_lock_with_cron_id(ctx, cron_id, worker_id).await? {
Some(cron) => match cron.exec_cron(ctx).await {
Ok(()) => {
tracing::debug!("Cron {cron_id} executed successfully");
cron.mark_cron_completed(ctx).await?;
}
Err(e) => {
tracing::error!("Error executing cron {cron_id}: {e}");
cron.mark_cron_failed(ctx, &e.to_string(), retry_duration)
.await?;
}
},
None => {
tracing::debug!(
"Cron lock not acquired for cron {cron_id} and worker {worker_id}, skipping..."
);
}
}
Ok(())
}
async fn try_acquire_lock_with_cron_id(
ctx: &dyn AppContextTrait,
cron_id: i32,
worker_id: &str,
) -> RecorderResult<Option<Self>> {
let db = ctx.db();
let txn = db.begin().await?;
let cron = Entity::find_by_id(cron_id)
.lock_with_behavior(LockType::Update, LockBehavior::SkipLocked)
.one(&txn)
.await?;
if let Some(cron) = cron {
if cron.enabled
&& cron.attempts < cron.max_attempts
&& cron.status == CronStatus::Pending
&& (cron.locked_at.is_none_or(|locked_at| {
locked_at + chrono::Duration::milliseconds(cron.timeout_ms as i64) <= Utc::now()
}))
&& cron.next_run.is_some_and(|next_run| next_run <= Utc::now())
{
let cron_active_model = ActiveModel {
id: Set(cron.id),
locked_by: Set(Some(worker_id.to_string())),
locked_at: Set(Some(Utc::now())),
status: Set(CronStatus::Running),
attempts: Set(cron.attempts + 1),
..Default::default()
};
let cron_model = cron_active_model.update(&txn).await?;
txn.commit().await?;
return Ok(Some(cron_model));
}
txn.commit().await?;
return Ok(Some(cron));
}
txn.rollback().await?;
Ok(None)
}
async fn exec_cron(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
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(())
}
async fn mark_cron_completed(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
let db = ctx.db();
let next_run = Self::calculate_next_run(&self.cron_expr)?;
ActiveModel {
id: Set(self.id),
next_run: Set(Some(next_run)),
last_run: Set(Some(Utc::now())),
status: Set(CronStatus::Pending),
locked_by: Set(None),
locked_at: Set(None),
attempts: Set(0),
last_error: Set(None),
..Default::default()
}
.update(db)
.await?;
Ok(())
}
async fn mark_cron_failed(
&self,
ctx: &dyn AppContextTrait,
error: &str,
retry_duration: chrono::Duration,
) -> RecorderResult<()> {
let db = ctx.db();
let should_retry = self.attempts < self.max_attempts;
let status = if should_retry {
CronStatus::Pending
} else {
CronStatus::Failed
};
let next_run = if should_retry {
Some(Utc::now() + retry_duration)
} else {
Some(Self::calculate_next_run(&self.cron_expr)?)
};
ActiveModel {
id: Set(self.id),
next_run: Set(next_run),
status: Set(status),
locked_by: Set(None),
locked_at: Set(None),
last_run: Set(Some(Utc::now())),
last_error: Set(Some(error.to_string())),
attempts: Set(if should_retry { self.attempts + 1 } else { 0 }),
..Default::default()
}
.update(db)
.await?;
Ok(())
}
pub async fn check_and_trigger_due_crons(ctx: &dyn AppContextTrait) -> RecorderResult<()> {
let db = ctx.db();
db.execute(Statement::from_string(
db.get_database_backend(),
format!("SELECT {CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME}()"),
))
.await?;
Ok(())
}
pub async fn check_and_cleanup_expired_cron_locks(
ctx: &dyn AppContextTrait,
retry_duration: chrono::Duration,
) -> RecorderResult<()> {
let db = ctx.db();
let condition = Condition::all()
.add(Column::Status.eq(CronStatus::Running))
.add(Column::LastRun.is_not_null())
.add(Column::TimeoutMs.is_not_null())
.add(
Expr::col(Column::LastRun)
.add(Expr::col(Column::TimeoutMs).mul(Expr::cust("INTERVAL '1 millisecond'")))
.lte(Expr::current_timestamp()),
);
let cron_ids = Entity::find()
.select_only()
.column(Column::Id)
.filter(condition.clone())
.lock_with_behavior(LockType::Update, LockBehavior::SkipLocked)
.into_tuple::<i32>()
.all(db)
.await?;
for cron_id in cron_ids {
let txn = db.begin().await?;
let locked_cron = Entity::find_by_id(cron_id)
.filter(condition.clone())
.lock_with_behavior(LockType::Update, LockBehavior::SkipLocked)
.one(&txn)
.await?;
if let Some(locked_cron) = locked_cron {
locked_cron
.mark_cron_failed(
ctx,
format!("Cron timeout of {}ms", locked_cron.timeout_ms).as_str(),
retry_duration,
)
.await?;
txn.commit().await?;
} else {
txn.rollback().await?;
}
}
Ok(())
}
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)?;
Ok(next)
}
}

View File

@@ -0,0 +1 @@

View File

@@ -129,7 +129,7 @@ pub enum RelatedEntity {
} }
impl ActiveModel { impl ActiveModel {
#[tracing::instrument(err, skip(ctx), fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))] #[tracing::instrument(err, skip_all, fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))]
pub fn from_mikan_bangumi_and_episode_meta( pub fn from_mikan_bangumi_and_episode_meta(
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
bangumi: &bangumi::Model, bangumi: &bangumi::Model,

View File

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

View File

@@ -1,5 +1,7 @@
use rss::Channel; use rss::Channel;
use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait}; use sea_orm::{
ColumnTrait, EntityTrait, JoinType, Order, QueryFilter, QueryOrder, QuerySelect, RelationTrait,
};
use url::Url; use url::Url;
use crate::{ use crate::{
@@ -37,13 +39,12 @@ impl Feed {
subscription_episode::Relation::Subscription.def(), subscription_episode::Relation::Subscription.def(),
) )
.filter(subscriptions::Column::Id.eq(subscription_id)) .filter(subscriptions::Column::Id.eq(subscription_id))
.order_by(episodes::Column::EnclosurePubDate, Order::Desc)
.all(db) .all(db)
.await?; .await?;
(subscription, episodes) (subscription, episodes)
} else { } else {
return Err(RecorderError::ModelEntityNotFound { return Err(RecorderError::from_entity_not_found::<subscriptions::Entity>());
entity: "Subscription".into(),
});
}; };
Ok(Feed::SubscritpionEpisodes( Ok(Feed::SubscritpionEpisodes(

View File

@@ -24,6 +24,7 @@ pub trait RssFeedItemTrait: Sized {
-> Option<Cow<'_, str>>; -> Option<Cow<'_, str>>;
fn get_enclosure_pub_date(&self) -> Option<DateTime<Utc>>; fn get_enclosure_pub_date(&self) -> Option<DateTime<Utc>>;
fn get_enclosure_content_length(&self) -> Option<i64>; fn get_enclosure_content_length(&self) -> Option<i64>;
fn get_xmlns(&self) -> Cow<'_, str>;
fn into_item(self, ctx: &dyn AppContextTrait, api_base: &Url) -> RecorderResult<Item> { fn into_item(self, ctx: &dyn AppContextTrait, api_base: &Url) -> RecorderResult<Item> {
let enclosure_mime_type = let enclosure_mime_type =
self.get_enclosure_mime() self.get_enclosure_mime()
@@ -53,21 +54,30 @@ pub trait RssFeedItemTrait: Sized {
let mut extensions = ExtensionMap::default(); let mut extensions = ExtensionMap::default();
if enclosure_mime_type == BITTORRENT_MIME_TYPE { if enclosure_mime_type == BITTORRENT_MIME_TYPE {
extensions.insert("torrent".to_string(), { let xmlns = self.get_xmlns();
let mut map = btreemap! {
let torrent_extension = ExtensionBuilder::default()
.name("torrent")
.attrs(btreemap! {
"xmlns".to_string() => xmlns.to_string()
})
.children({
let mut m = btreemap! {
"link".to_string() => vec![ "link".to_string() => vec![
ExtensionBuilder::default().name( ExtensionBuilder::default()
"link" .name("link")
).value(enclosure_link.to_string()).build() .value(link.to_string())
.build()
], ],
"contentLength".to_string() => vec![ "contentLength".to_string() => vec![
ExtensionBuilder::default().name( ExtensionBuilder::default()
"contentLength" .name("contentLength")
).value(enclosure_content_length.to_string()).build() .value(enclosure_content_length.to_string())
], .build()
]
}; };
if let Some(pub_date) = enclosure_pub_date { if let Some(pub_date) = enclosure_pub_date {
map.insert( m.insert(
"pubDate".to_string(), "pubDate".to_string(),
vec![ vec![
ExtensionBuilder::default() ExtensionBuilder::default()
@@ -76,9 +86,17 @@ pub trait RssFeedItemTrait: Sized {
.build(), .build(),
], ],
); );
} };
map m
}); })
.build();
extensions.insert(
"".to_string(),
btreemap! {
"torrent".to_string() => vec![torrent_extension]
},
);
}; };
let enclosure = EnclosureBuilder::default() let enclosure = EnclosureBuilder::default()

View File

@@ -42,6 +42,12 @@ impl RssFeedItemTrait for episodes::Model {
Cow::Owned(format!("{PROJECT_NAME}:episode:{}", self.id)) Cow::Owned(format!("{PROJECT_NAME}:episode:{}", self.id))
} }
fn get_xmlns(&self) -> Cow<'_, str> {
match self.episode_type {
episodes::EpisodeType::Mikan => Cow::Borrowed("https://mikanani.me/0.1/"),
}
}
fn get_title(&self) -> Cow<'_, str> { fn get_title(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.display_name) Cow::Borrowed(&self.display_name)
} }

View File

@@ -11,3 +11,4 @@ pub mod subscribers;
pub mod subscription_bangumi; pub mod subscription_bangumi;
pub mod subscription_episode; pub mod subscription_episode;
pub mod subscriptions; pub mod subscriptions;
pub mod cron;

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::{ use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, Insert, IntoActiveModel, ActiveModelTrait, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, Insert, IntoActiveModel,
Iterable, QueryResult, QueryTrait, SelectModel, SelectorRaw, sea_query::Query, QueryResult, QueryTrait, sea_query::Query,
}; };
#[async_trait] #[async_trait]
@@ -10,13 +10,6 @@ where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>, <A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait, A: ActiveModelTrait,
{ {
fn exec_with_returning_models<C>(
self,
db: &C,
) -> SelectorRaw<SelectModel<<A::Entity as EntityTrait>::Model>>
where
C: ConnectionTrait;
async fn exec_with_returning_columns<C, I>( async fn exec_with_returning_columns<C, I>(
self, self,
db: &C, db: &C,
@@ -33,26 +26,6 @@ where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>, <A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait + Send, A: ActiveModelTrait + Send,
{ {
fn exec_with_returning_models<C>(
self,
db: &C,
) -> SelectorRaw<SelectModel<<A::Entity as EntityTrait>::Model>>
where
C: ConnectionTrait,
{
let mut insert_statement = self.into_query();
let db_backend = db.get_database_backend();
let returning = Query::returning().exprs(
<A::Entity as EntityTrait>::Column::iter()
.map(|c| c.select_as(c.into_returning_expr(db_backend))),
);
insert_statement.returning(returning);
let insert_statement = db_backend.build(&insert_statement);
SelectorRaw::<SelectModel<<A::Entity as EntityTrait>::Model>>::from_statement(
insert_statement,
)
}
async fn exec_with_returning_columns<C, I>( async fn exec_with_returning_columns<C, I>(
self, self,
db: &C, db: &C,

View File

@@ -29,6 +29,7 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: String, pub id: String,
pub subscriber_id: i32, pub subscriber_id: i32,
pub subscription_id: Option<i32>,
pub job: SubscriberTask, pub job: SubscriberTask,
pub task_type: SubscriberTaskType, pub task_type: SubscriberTaskType,
pub status: SubscriberTaskStatus, pub status: SubscriberTaskStatus,
@@ -52,6 +53,14 @@ pub enum Relation {
on_delete = "Cascade" on_delete = "Cascade"
)] )]
Subscriber, Subscriber,
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Subscription,
} }
impl Related<super::subscribers::Entity> for Entity { impl Related<super::subscribers::Entity> for Entity {
@@ -60,10 +69,18 @@ impl Related<super::subscribers::Entity> for Entity {
} }
} }
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity { pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")] #[sea_orm(entity = "super::subscribers::Entity")]
Subscriber, Subscriber,
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
} }
#[async_trait] #[async_trait]

View File

@@ -130,10 +130,9 @@ impl Model {
pub async fn find_by_id(ctx: &dyn AppContextTrait, id: i32) -> RecorderResult<Self> { pub async fn find_by_id(ctx: &dyn AppContextTrait, id: i32) -> RecorderResult<Self> {
let db = ctx.db(); let db = ctx.db();
let subscriber = Entity::find_by_id(id) let subscriber = Entity::find_by_id(id).one(db).await?.ok_or_else(|| {
.one(db) RecorderError::from_entity_not_found_detail::<Entity, _>(format!("id {id} not found"))
.await? })?;
.ok_or_else(|| RecorderError::from_db_record_not_found("subscribers::find_by_id"))?;
Ok(subscriber) Ok(subscriber)
} }

View File

@@ -11,10 +11,7 @@ pub use registry::{
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{app::AppContextTrait, errors::RecorderResult};
app::AppContextTrait,
errors::{RecorderError, RecorderResult},
};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscriptions")] #[sea_orm(table_name = "subscriptions")]
@@ -61,6 +58,8 @@ pub enum Relation {
Credential3rd, Credential3rd,
#[sea_orm(has_many = "super::feeds::Entity")] #[sea_orm(has_many = "super::feeds::Entity")]
Feed, Feed,
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
SubscriberTask,
} }
impl Related<super::subscribers::Entity> for Entity { impl Related<super::subscribers::Entity> for Entity {
@@ -121,6 +120,12 @@ impl Related<super::credential_3rd::Entity> for Entity {
} }
} }
impl Related<super::subscriber_tasks::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriberTask.def()
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity { pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")] #[sea_orm(entity = "super::subscribers::Entity")]
@@ -137,6 +142,8 @@ pub enum RelatedEntity {
Credential3rd, Credential3rd,
#[sea_orm(entity = "super::feeds::Entity")] #[sea_orm(entity = "super::feeds::Entity")]
Feed, Feed,
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
SubscriberTask,
} }
#[async_trait] #[async_trait]
@@ -145,51 +152,7 @@ impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {} impl ActiveModel {}
impl Model { impl Model {
pub async fn toggle_with_ids( pub async fn exec_cron(&self, _ctx: &dyn AppContextTrait) -> RecorderResult<()> {
ctx: &dyn AppContextTrait, todo!()
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::ModelEntityNotFound {
entity: "Subscription".into(),
})?;
if subscription_model.subscriber_id != subscriber_id {
Err(RecorderError::ModelEntityNotFound {
entity: "Subscription".into(),
})?;
}
Ok(subscription_model)
} }
} }

View File

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

View File

@@ -209,7 +209,7 @@ impl StorageService {
lister.try_collect().await lister.try_collect().await
} }
#[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = ?accept))] #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = accept.to_string()))]
pub async fn serve_optimized_image( pub async fn serve_optimized_image(
&self, &self,
storage_path: impl AsRef<Path>, storage_path: impl AsRef<Path>,

View File

@@ -8,10 +8,12 @@ pub struct TaskConfig {
pub subscriber_task_concurrency: u32, pub subscriber_task_concurrency: u32,
#[serde(default = "default_system_task_workers")] #[serde(default = "default_system_task_workers")]
pub system_task_concurrency: u32, pub system_task_concurrency: u32,
#[serde(default = "default_subscriber_task_timeout")] #[serde(default = "default_subscriber_task_reenqueue_orphaned_after")]
pub subscriber_task_timeout: Duration, pub subscriber_task_reenqueue_orphaned_after: Duration,
#[serde(default = "default_system_task_timeout")] #[serde(default = "default_system_task_reenqueue_orphaned_after")]
pub system_task_timeout: Duration, pub system_task_reenqueue_orphaned_after: Duration,
#[serde(default = "default_cron_retry_duration")]
pub cron_retry_duration: Duration,
} }
impl Default for TaskConfig { impl Default for TaskConfig {
@@ -19,8 +21,10 @@ impl Default for TaskConfig {
Self { Self {
subscriber_task_concurrency: default_subscriber_task_workers(), subscriber_task_concurrency: default_subscriber_task_workers(),
system_task_concurrency: default_system_task_workers(), system_task_concurrency: default_system_task_workers(),
subscriber_task_timeout: default_subscriber_task_timeout(), subscriber_task_reenqueue_orphaned_after:
system_task_timeout: default_system_task_timeout(), default_subscriber_task_reenqueue_orphaned_after(),
system_task_reenqueue_orphaned_after: default_system_task_reenqueue_orphaned_after(),
cron_retry_duration: default_cron_retry_duration(),
} }
} }
} }
@@ -41,10 +45,14 @@ pub fn default_system_task_workers() -> u32 {
} }
} }
pub fn default_subscriber_task_timeout() -> Duration { pub fn default_subscriber_task_reenqueue_orphaned_after() -> Duration {
Duration::from_secs(3600) Duration::from_secs(3600)
} }
pub fn default_system_task_timeout() -> Duration { pub fn default_system_task_reenqueue_orphaned_after() -> Duration {
Duration::from_secs(3600) Duration::from_secs(3600)
} }
pub fn default_cron_retry_duration() -> Duration {
Duration::from_secs(5)
}

View File

@@ -1,34 +1,56 @@
use std::sync::Arc; use std::sync::Arc;
use futures::Stream; use async_trait::async_trait;
use serde::{Serialize, de::DeserializeOwned}; use futures::{Stream, StreamExt, pin_mut};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::{app::AppContextTrait, errors::RecorderResult}; use crate::{app::AppContextTrait, errors::RecorderResult};
pub const SYSTEM_TASK_APALIS_NAME: &str = "system_task"; pub const SYSTEM_TASK_APALIS_NAME: &str = "system_task";
pub const SUBSCRIBER_TASK_APALIS_NAME: &str = "subscriber_task"; pub const SUBSCRIBER_TASK_APALIS_NAME: &str = "subscriber_task";
#[async_trait::async_trait] #[async_trait]
pub trait AsyncTaskTrait: Serialize + DeserializeOwned + Sized { pub trait AsyncTaskTrait: Serialize + DeserializeOwned + Sized {
async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>; async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
async fn run(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
self.run_async(ctx).await?;
Ok(())
}
} }
#[async_trait::async_trait] pub trait StreamTaskTrait {
pub trait StreamTaskTrait: Serialize + DeserializeOwned + Sized {
type Yield: Serialize + DeserializeOwned + Send; type Yield: Serialize + DeserializeOwned + Send;
fn run_stream( fn run_stream(
self, self,
ctx: Arc<dyn AppContextTrait>, ctx: Arc<dyn AppContextTrait>,
) -> impl Stream<Item = RecorderResult<Self::Yield>> + Send; ) -> impl Stream<Item = RecorderResult<Self::Yield>> + Send;
}
async fn run(self, _ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> { #[async_trait]
unimplemented!() impl<T> AsyncTaskTrait for T
where
T: StreamTaskTrait + Serialize + DeserializeOwned + Sized + Send,
{
async fn run_async(self, _ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
let s = self.run_stream(_ctx);
pin_mut!(s);
while let Some(item) = s.next().await {
item?;
}
Ok(())
} }
} }
pub trait SubscriberTaskTrait: AsyncTaskTrait {
fn get_subscriber_id(&self) -> i32;
fn get_cron_id(&self) -> Option<i32>;
}
pub trait SystemTaskTrait: AsyncTaskTrait {}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct SubscriberTaskBase {
pub subscriber_id: i32,
pub cron_id: Option<i32>,
}

View File

@@ -6,6 +6,7 @@ mod service;
pub use core::{ pub use core::{
AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, StreamTaskTrait, AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, StreamTaskTrait,
SubscriberTaskBase, SubscriberTaskTrait, SystemTaskTrait,
}; };
pub use config::TaskConfig; pub use config::TaskConfig;

View File

@@ -1,134 +1,12 @@
mod media; mod subscriber;
mod subscription; mod system;
use std::sync::Arc;
pub use media::OptimizeImageTask; pub use subscriber::{
use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult}; SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant,
use serde::{Deserialize, Serialize}; SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask,
pub use subscription::{ SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask,
SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask,
SyncOneSubscriptionSourcesTask,
}; };
pub use system::{
use crate::{ OptimizeImageTask, SystemTask, SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant,
app::AppContextTrait, SystemTaskTypeVariantIter,
errors::{RecorderError, RecorderResult},
models::subscriptions::SubscriptionTrait,
task::AsyncTaskTrait,
}; };
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Copy,
DeriveActiveEnum,
DeriveDisplay,
EnumIter,
)]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum SubscriberTaskType {
#[serde(rename = "sync_one_subscription_feeds_incremental")]
#[sea_orm(string_value = "sync_one_subscription_feeds_incremental")]
SyncOneSubscriptionFeedsIncremental,
#[serde(rename = "sync_one_subscription_feeds_full")]
#[sea_orm(string_value = "sync_one_subscription_feeds_full")]
SyncOneSubscriptionFeedsFull,
#[serde(rename = "sync_one_subscription_sources")]
#[sea_orm(string_value = "sync_one_subscription_sources")]
SyncOneSubscriptionSources,
}
impl TryFrom<&SubscriberTask> for serde_json::Value {
type Error = RecorderError;
fn try_from(value: &SubscriberTask) -> Result<Self, Self::Error> {
let json_value = serde_json::to_value(value)?;
Ok(match json_value {
serde_json::Value::Object(mut map) => {
map.remove("task_type");
serde_json::Value::Object(map)
}
_ => {
unreachable!("subscriber task must be an json object");
}
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, FromJsonQueryResult)]
#[serde(tag = "task_type")]
pub enum SubscriberTask {
#[serde(rename = "sync_one_subscription_feeds_incremental")]
SyncOneSubscriptionFeedsIncremental(SyncOneSubscriptionFeedsIncrementalTask),
#[serde(rename = "sync_one_subscription_feeds_full")]
SyncOneSubscriptionFeedsFull(SyncOneSubscriptionFeedsFullTask),
#[serde(rename = "sync_one_subscription_sources")]
SyncOneSubscriptionSources(SyncOneSubscriptionSourcesTask),
}
impl SubscriberTask {
pub fn get_subscriber_id(&self) -> i32 {
match self {
Self::SyncOneSubscriptionFeedsIncremental(task) => task.0.get_subscriber_id(),
Self::SyncOneSubscriptionFeedsFull(task) => task.0.get_subscriber_id(),
Self::SyncOneSubscriptionSources(task) => task.0.get_subscriber_id(),
}
}
pub async fn run(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
match self {
Self::SyncOneSubscriptionFeedsIncremental(task) => task.run(ctx).await,
Self::SyncOneSubscriptionFeedsFull(task) => task.run(ctx).await,
Self::SyncOneSubscriptionSources(task) => task.run(ctx).await,
}
}
pub fn task_type(&self) -> SubscriberTaskType {
match self {
Self::SyncOneSubscriptionFeedsIncremental(_) => {
SubscriberTaskType::SyncOneSubscriptionFeedsIncremental
}
Self::SyncOneSubscriptionFeedsFull(_) => {
SubscriberTaskType::SyncOneSubscriptionFeedsFull
}
Self::SyncOneSubscriptionSources(_) => SubscriberTaskType::SyncOneSubscriptionSources,
}
}
}
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Copy,
DeriveActiveEnum,
DeriveDisplay,
EnumIter,
)]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum SystemTaskType {
#[serde(rename = "optimize_image")]
#[sea_orm(string_value = "optimize_image")]
OptimizeImage,
}
#[derive(Clone, Debug, Serialize, Deserialize, FromJsonQueryResult)]
pub enum SystemTask {
#[serde(rename = "optimize_image")]
OptimizeImage(OptimizeImageTask),
}
impl SystemTask {
pub async fn run(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
match self {
Self::OptimizeImage(task) => task.run(ctx).await,
}
}
}

View File

@@ -0,0 +1,29 @@
macro_rules! register_subscriber_task_type {
(
$(#[$type_meta:meta])*
$task_vis:vis struct $task_name:ident {
$($(#[$field_meta:meta])* pub $field_name:ident: $field_type:ty),* $(,)?
}
) => {
$(#[$type_meta])*
#[derive(typed_builder::TypedBuilder)]
$task_vis struct $task_name {
$($(#[$field_meta])* pub $field_name: $field_type,)*
pub subscriber_id: i32,
#[builder(default = None)]
pub cron_id: Option<i32>,
}
impl $crate::task::SubscriberTaskTrait for $task_name {
fn get_subscriber_id(&self) -> i32 {
self.subscriber_id
}
fn get_cron_id(&self) -> Option<i32> {
self.cron_id
}
}
}
}
pub(crate) use register_subscriber_task_type;

View File

@@ -0,0 +1,143 @@
mod base;
mod subscription;
use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult};
use serde::{Deserialize, Serialize};
pub use subscription::{
SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask,
SyncOneSubscriptionSourcesTask,
};
macro_rules! register_subscriber_task_types {
(
task_type_enum: {
$(#[$type_enum_meta:meta])*
pub enum $type_enum_name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident => $string_value:literal
),* $(,)?
}
},
task_enum: {
$(#[$task_enum_meta:meta])*
pub enum $task_enum_name:ident {
$(
$(#[$task_variant_meta:meta])*
$task_variant:ident($task_type:ty)
),* $(,)?
}
}
) => {
$(#[$type_enum_meta])*
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum $type_enum_name {
$(
$(#[$variant_meta])*
#[serde(rename = $string_value)]
#[sea_orm(string_value = $string_value)]
$variant,
)*
}
$(#[$task_enum_meta])*
#[serde(tag = "task_type")]
pub enum $task_enum_name {
$(
$(#[$task_variant_meta])*
#[serde(rename = $string_value)]
$task_variant($task_type),
)*
}
impl TryFrom<$task_enum_name> for serde_json::Value {
type Error = $crate::errors::RecorderError;
fn try_from(value: $task_enum_name) -> Result<Self, Self::Error> {
let json_value = serde_json::to_value(value)?;
Ok(match json_value {
serde_json::Value::Object(mut map) => {
map.remove("task_type");
serde_json::Value::Object(map)
}
_ => {
unreachable!("subscriber task must be an json object");
}
})
}
}
impl $task_enum_name {
pub fn task_type(&self) -> $type_enum_name {
match self {
$(Self::$task_variant(_) => $type_enum_name::$variant,)*
}
}
}
#[async_trait::async_trait]
impl $crate::task::AsyncTaskTrait for $task_enum_name {
async fn run_async(self, ctx: std::sync::Arc<dyn $crate::app::AppContextTrait>) -> $crate::errors::RecorderResult<()> {
match self {
$(Self::$task_variant(t) =>
<$task_type as $crate::task::AsyncTaskTrait>::run_async(t, ctx).await,)*
}
}
}
impl $crate::task::SubscriberTaskTrait for $task_enum_name {
fn get_subscriber_id(&self) -> i32 {
match self {
$(Self::$task_variant(t) =>
<$task_type as $crate::task::SubscriberTaskTrait>::get_subscriber_id(t),)*
}
}
fn get_cron_id(&self) -> Option<i32> {
match self {
$(Self::$task_variant(t) =>
<$task_type as $crate::task::SubscriberTaskTrait>::get_cron_id(t),)*
}
}
}
$(
impl From<$task_type> for $task_enum_name {
fn from(task: $task_type) -> Self {
Self::$task_variant(task)
}
}
)*
};
}
register_subscriber_task_types!(
task_type_enum: {
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Copy,
DeriveActiveEnum,
DeriveDisplay,
EnumIter,
)]
pub enum SubscriberTaskType {
SyncOneSubscriptionFeedsIncremental => "sync_one_subscription_feeds_incremental",
SyncOneSubscriptionFeedsFull => "sync_one_subscription_feeds_full",
SyncOneSubscriptionSources => "sync_one_subscription_sources"
}
},
task_enum: {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, FromJsonQueryResult)]
pub enum SubscriberTask {
SyncOneSubscriptionFeedsIncremental(SyncOneSubscriptionFeedsIncrementalTask),
SyncOneSubscriptionFeedsFull(SyncOneSubscriptionFeedsFullTask),
SyncOneSubscriptionSources(SyncOneSubscriptionSourcesTask),
}
}
);

View File

@@ -0,0 +1,67 @@
use sea_orm::prelude::*;
use serde::{Deserialize, Serialize};
use super::base::register_subscriber_task_type;
use crate::{errors::RecorderResult, models::subscriptions::SubscriptionTrait};
macro_rules! register_subscription_task_type {
(
$(#[$type_meta:meta])* pub struct $task_name:ident {
$($(#[$field_meta:meta])* pub $field_name:ident: $field_type:ty),* $(,)?
} => async |$subscription_param:ident, $ctx_param:ident| -> $task_return_type:ty $method_body:block
) => {
register_subscriber_task_type! {
$(#[$type_meta])*
pub struct $task_name {
$($(#[$field_meta])* pub $field_name: $field_type,)*
pub subscription_id: i32,
}
}
#[async_trait::async_trait]
impl $crate::task::AsyncTaskTrait for $task_name {
async fn run_async(self, ctx: std::sync::Arc<dyn $crate::app::AppContextTrait>) -> $task_return_type {
use $crate::models::subscriptions::{
Entity, Column, Subscription,
};
let subscription_model = Entity::find()
.filter(Column::Id.eq(self.subscription_id))
.filter(Column::SubscriberId.eq(self.subscriber_id))
.one(ctx.db())
.await?
.ok_or_else(|| $crate::errors::RecorderError::from_entity_not_found::<Entity>())?;
let $subscription_param = Subscription::try_from_model(&subscription_model)?;
let $ctx_param = ctx;
$method_body
}
}
}
}
register_subscription_task_type! {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionFeedsIncrementalTask {
} => async |subscription, ctx| -> RecorderResult<()> {
subscription.sync_feeds_incremental(ctx).await?;
Ok(())
}
}
register_subscription_task_type! {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionFeedsFullTask {
} => async |subscription, ctx| -> RecorderResult<()> {
subscription.sync_feeds_full(ctx).await?;
Ok(())
}
}
register_subscription_task_type! {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionSourcesTask {
} => async |subscription, ctx| -> RecorderResult<()> {
subscription.sync_sources(ctx).await?;
Ok(())
}
}

View File

@@ -1,62 +0,0 @@
use std::sync::Arc;
use sea_orm::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{
app::AppContextTrait,
errors::RecorderResult,
models::subscriptions::{self, SubscriptionTrait},
task::AsyncTaskTrait,
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionFeedsIncrementalTask(pub subscriptions::Subscription);
impl From<subscriptions::Subscription> for SyncOneSubscriptionFeedsIncrementalTask {
fn from(subscription: subscriptions::Subscription) -> Self {
Self(subscription)
}
}
#[async_trait::async_trait]
impl AsyncTaskTrait for SyncOneSubscriptionFeedsIncrementalTask {
async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
self.0.sync_feeds_incremental(ctx).await?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionFeedsFullTask(pub subscriptions::Subscription);
impl From<subscriptions::Subscription> for SyncOneSubscriptionFeedsFullTask {
fn from(subscription: subscriptions::Subscription) -> Self {
Self(subscription)
}
}
#[async_trait::async_trait]
impl AsyncTaskTrait for SyncOneSubscriptionFeedsFullTask {
async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
self.0.sync_feeds_full(ctx).await?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOneSubscriptionSourcesTask(pub subscriptions::Subscription);
#[async_trait::async_trait]
impl AsyncTaskTrait for SyncOneSubscriptionSourcesTask {
async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
self.0.sync_sources(ctx).await?;
Ok(())
}
}
impl From<subscriptions::Subscription> for SyncOneSubscriptionSourcesTask {
fn from(subscription: subscriptions::Subscription) -> Self {
Self(subscription)
}
}

View File

@@ -0,0 +1,108 @@
mod media;
pub use media::OptimizeImageTask;
use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult};
use serde::{Deserialize, Serialize};
macro_rules! register_system_task_types {
(
task_type_enum: {
$(#[$type_enum_meta:meta])*
pub enum $type_enum_name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident => $string_value:literal
),* $(,)?
}
},
task_enum: {
$(#[$task_enum_meta:meta])*
pub enum $task_enum_name:ident {
$(
$task_variant:ident($task_type:ty)
),* $(,)?
}
}
) => {
$(#[$type_enum_meta])*
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum $type_enum_name {
$(
$(#[$variant_meta])*
#[serde(rename = $string_value)]
#[sea_orm(string_value = $string_value)]
$variant,
)*
}
$(#[$task_enum_meta])*
#[serde(tag = "task_type")]
pub enum $task_enum_name {
$(
$task_variant($task_type),
)*
}
impl TryFrom<$task_enum_name> for serde_json::Value {
type Error = $crate::errors::RecorderError;
fn try_from(value: $task_enum_name) -> Result<Self, Self::Error> {
let json_value = serde_json::to_value(value)?;
Ok(match json_value {
serde_json::Value::Object(mut map) => {
map.remove("task_type");
serde_json::Value::Object(map)
}
_ => {
unreachable!("subscriber task must be an json object");
}
})
}
}
impl $task_enum_name {
pub fn task_type(&self) -> $type_enum_name {
match self {
$(Self::$task_variant(_) => $type_enum_name::$variant,)*
}
}
}
#[async_trait::async_trait]
impl $crate::task::AsyncTaskTrait for $task_enum_name {
async fn run_async(self, ctx: std::sync::Arc<dyn $crate::app::AppContextTrait>) -> $crate::errors::RecorderResult<()> {
match self {
$(Self::$task_variant(t) =>
<$task_type as $crate::task::AsyncTaskTrait>::run_async(t, ctx).await,)*
}
}
}
};
}
register_system_task_types! {
task_type_enum: {
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Copy,
DeriveActiveEnum,
DeriveDisplay,
EnumIter,
)]
pub enum SystemTaskType {
OptimizeImage => "optimize_image"
}
},
task_enum: {
#[derive(Clone, Debug, Serialize, Deserialize, FromJsonQueryResult)]
pub enum SystemTask {
OptimizeImage(OptimizeImageTask),
}
}
}

View File

@@ -1,18 +1,21 @@
use std::{ops::Deref, str::FromStr, sync::Arc}; use std::{future::Future, ops::Deref, str::FromStr, sync::Arc};
use apalis::prelude::*; use apalis::prelude::*;
use apalis_sql::{ use apalis_sql::{
Config, Config,
context::SqlContext, context::SqlContext,
postgres::{PgListen, PostgresStorage}, postgres::{PgListen as ApalisPgListen, PostgresStorage as ApalisPostgresStorage},
}; };
use sea_orm::sqlx::postgres::PgListener;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
app::AppContextTrait, app::AppContextTrait,
errors::{RecorderError, RecorderResult}, errors::{RecorderError, RecorderResult},
models::cron::{self, CRON_DUE_EVENT},
task::{ task::{
SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask, TaskConfig, AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask,
TaskConfig,
config::{default_subscriber_task_workers, default_system_task_workers}, config::{default_subscriber_task_workers, default_system_task_workers},
registry::SystemTask, registry::SystemTask,
}, },
@@ -21,8 +24,9 @@ use crate::{
pub struct TaskService { pub struct TaskService {
pub config: TaskConfig, pub config: TaskConfig,
ctx: Arc<dyn AppContextTrait>, ctx: Arc<dyn AppContextTrait>,
subscriber_task_storage: Arc<RwLock<PostgresStorage<SubscriberTask>>>, subscriber_task_storage: Arc<RwLock<ApalisPostgresStorage<SubscriberTask>>>,
system_task_storage: Arc<RwLock<PostgresStorage<SystemTask>>>, system_task_storage: Arc<RwLock<ApalisPostgresStorage<SystemTask>>>,
cron_worker_id: String,
} }
impl TaskService { impl TaskService {
@@ -38,17 +42,18 @@ impl TaskService {
}; };
let pool = ctx.db().get_postgres_connection_pool().clone(); let pool = ctx.db().get_postgres_connection_pool().clone();
let subscriber_task_storage_config = let subscriber_task_storage_config = Config::new(SUBSCRIBER_TASK_APALIS_NAME)
Config::new(SUBSCRIBER_TASK_APALIS_NAME).set_keep_alive(config.subscriber_task_timeout); .set_reenqueue_orphaned_after(config.subscriber_task_reenqueue_orphaned_after);
let system_task_storage_config = let system_task_storage_config = Config::new(SYSTEM_TASK_APALIS_NAME)
Config::new(SYSTEM_TASK_APALIS_NAME).set_keep_alive(config.system_task_timeout); .set_reenqueue_orphaned_after(config.system_task_reenqueue_orphaned_after);
let subscriber_task_storage = let subscriber_task_storage =
PostgresStorage::new_with_config(pool.clone(), subscriber_task_storage_config); ApalisPostgresStorage::new_with_config(pool.clone(), subscriber_task_storage_config);
let system_task_storage = let system_task_storage =
PostgresStorage::new_with_config(pool, system_task_storage_config); ApalisPostgresStorage::new_with_config(pool, system_task_storage_config);
Ok(Self { Ok(Self {
config, config,
cron_worker_id: nanoid::nanoid!(),
ctx, ctx,
subscriber_task_storage: Arc::new(RwLock::new(subscriber_task_storage)), subscriber_task_storage: Arc::new(RwLock::new(subscriber_task_storage)),
system_task_storage: Arc::new(RwLock::new(system_task_storage)), system_task_storage: Arc::new(RwLock::new(system_task_storage)),
@@ -61,7 +66,7 @@ impl TaskService {
) -> RecorderResult<()> { ) -> RecorderResult<()> {
let ctx = data.deref().clone(); let ctx = data.deref().clone();
job.run(ctx).await job.run_async(ctx).await
} }
async fn run_system_task( async fn run_system_task(
@@ -69,7 +74,7 @@ impl TaskService {
data: Data<Arc<dyn AppContextTrait>>, data: Data<Arc<dyn AppContextTrait>>,
) -> RecorderResult<()> { ) -> RecorderResult<()> {
let ctx = data.deref().clone(); let ctx = data.deref().clone();
job.run(ctx).await job.run_async(ctx).await
} }
pub async fn retry_subscriber_task(&self, job_id: String) -> RecorderResult<()> { pub async fn retry_subscriber_task(&self, job_id: String) -> RecorderResult<()> {
@@ -100,7 +105,6 @@ impl TaskService {
pub async fn add_subscriber_task( pub async fn add_subscriber_task(
&self, &self,
_subscriber_id: i32,
subscriber_task: SubscriberTask, subscriber_task: SubscriberTask,
) -> RecorderResult<TaskId> { ) -> RecorderResult<TaskId> {
let task_id = { let task_id = {
@@ -132,8 +136,88 @@ impl TaskService {
Ok(task_id) Ok(task_id)
} }
pub async fn setup_monitor(&self) -> RecorderResult<Monitor> { pub async fn run<F, Fut>(&self, shutdown_signal: Option<F>) -> RecorderResult<()>
let mut monitor = Monitor::new(); where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send,
{
tokio::try_join!(
async {
let monitor = self.setup_apalis_monitor().await?;
if let Some(shutdown_signal) = shutdown_signal {
monitor
.run_with_signal(async move {
shutdown_signal().await;
tracing::info!("apalis shutting down...");
Ok(())
})
.await?;
} else {
monitor.run().await?;
}
Ok::<_, RecorderError>(())
},
async {
let listener = self.setup_apalis_listener().await?;
tokio::task::spawn(async move {
if let Err(e) = listener.listen().await {
tracing::error!("Error listening to apalis: {e}");
}
});
Ok::<_, RecorderError>(())
},
async {
let listener = self.setup_cron_due_listening().await?;
let ctx = self.ctx.clone();
let cron_worker_id = self.cron_worker_id.clone();
let retry_duration = chrono::Duration::milliseconds(
self.config.cron_retry_duration.as_millis() as i64,
);
tokio::task::spawn(async move {
if let Err(e) =
Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration).await
{
tracing::error!("Error listening to cron due: {e}");
}
});
Ok::<_, RecorderError>(())
},
async {
let ctx = self.ctx.clone();
let retry_duration = chrono::Duration::milliseconds(
self.config.cron_retry_duration.as_millis() as i64,
);
tokio::task::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
loop {
interval.tick().await;
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
ctx.as_ref(),
retry_duration,
)
.await
{
tracing::error!(
"Error checking and cleaning up expired cron locks: {e}"
);
}
if let Err(e) = cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
{
tracing::error!("Error checking and triggering due crons: {e}");
}
}
});
Ok::<_, RecorderError>(())
}
)?;
Ok(())
}
async fn setup_apalis_monitor(&self) -> RecorderResult<Monitor> {
let mut apalis_monitor = Monitor::new();
{ {
let subscriber_task_worker = WorkerBuilder::new(SUBSCRIBER_TASK_APALIS_NAME) let subscriber_task_worker = WorkerBuilder::new(SUBSCRIBER_TASK_APALIS_NAME)
@@ -155,28 +239,58 @@ impl TaskService {
.backend(self.system_task_storage.read().await.clone()) .backend(self.system_task_storage.read().await.clone())
.build_fn(Self::run_system_task); .build_fn(Self::run_system_task);
monitor = monitor apalis_monitor = apalis_monitor
.register(subscriber_task_worker) .register(subscriber_task_worker)
.register(system_task_worker); .register(system_task_worker);
} }
Ok(monitor) Ok(apalis_monitor)
} }
pub async fn setup_listener(&self) -> RecorderResult<PgListen> { async fn setup_apalis_listener(&self) -> RecorderResult<ApalisPgListen> {
let pool = self.ctx.db().get_postgres_connection_pool().clone(); let pool = self.ctx.db().get_postgres_connection_pool().clone();
let mut task_listener = PgListen::new(pool).await?; let mut apalis_pg_listener = ApalisPgListen::new(pool).await?;
{ {
let mut subscriber_task_storage = self.subscriber_task_storage.write().await; let mut subscriber_task_storage = self.subscriber_task_storage.write().await;
task_listener.subscribe_with(&mut subscriber_task_storage); apalis_pg_listener.subscribe_with(&mut subscriber_task_storage);
} }
{ {
let mut system_task_storage = self.system_task_storage.write().await; let mut system_task_storage = self.system_task_storage.write().await;
task_listener.subscribe_with(&mut system_task_storage); apalis_pg_listener.subscribe_with(&mut system_task_storage);
} }
Ok(task_listener) Ok(apalis_pg_listener)
}
async fn setup_cron_due_listening(&self) -> RecorderResult<PgListener> {
let pool = self.ctx.db().get_postgres_connection_pool().clone();
let listener = PgListener::connect_with(&pool).await?;
Ok(listener)
}
async fn listen_cron_due(
mut listener: PgListener,
ctx: Arc<dyn AppContextTrait>,
worker_id: &str,
retry_duration: chrono::Duration,
) -> RecorderResult<()> {
listener.listen(CRON_DUE_EVENT).await?;
loop {
let notification = listener.recv().await?;
if let Err(e) = cron::Model::handle_cron_notification(
ctx.as_ref(),
notification,
worker_id,
retry_duration,
)
.await
{
tracing::error!("Error handling cron notification: {e}");
}
}
} }
} }

View File

@@ -9,12 +9,12 @@ pub trait ControllerTrait: Sized {
-> Router<Arc<dyn AppContextTrait>>; -> Router<Arc<dyn AppContextTrait>>;
} }
pub struct PrefixController { pub struct NestRouterController {
prefix: Cow<'static, str>, prefix: Cow<'static, str>,
router: Router<Arc<dyn AppContextTrait>>, router: Router<Arc<dyn AppContextTrait>>,
} }
impl PrefixController { impl NestRouterController {
pub fn new( pub fn new(
prefix: impl Into<Cow<'static, str>>, prefix: impl Into<Cow<'static, str>>,
router: Router<Arc<dyn AppContextTrait>>, router: Router<Arc<dyn AppContextTrait>>,
@@ -26,7 +26,7 @@ impl PrefixController {
} }
} }
impl ControllerTrait for PrefixController { impl ControllerTrait for NestRouterController {
fn apply_to( fn apply_to(
self, self,
router: Router<Arc<dyn AppContextTrait>>, router: Router<Arc<dyn AppContextTrait>>,
@@ -36,15 +36,15 @@ impl ControllerTrait for PrefixController {
} }
pub enum Controller { pub enum Controller {
Prefix(PrefixController), NestRouter(NestRouterController),
} }
impl Controller { impl Controller {
pub fn from_prefix( pub fn from_nest_router(
prefix: impl Into<Cow<'static, str>>, prefix: impl Into<Cow<'static, str>>,
router: Router<Arc<dyn AppContextTrait>>, router: Router<Arc<dyn AppContextTrait>>,
) -> Self { ) -> Self {
Self::Prefix(PrefixController::new(prefix, router)) Self::NestRouter(NestRouterController::new(prefix, router))
} }
} }
@@ -54,7 +54,7 @@ impl ControllerTrait for Controller {
router: Router<Arc<dyn AppContextTrait>>, router: Router<Arc<dyn AppContextTrait>>,
) -> Router<Arc<dyn AppContextTrait>> { ) -> Router<Arc<dyn AppContextTrait>> {
match self { match self {
Self::Prefix(p) => p.apply_to(router), Self::NestRouter(p) => p.apply_to(router),
} }
} }
} }

View File

@@ -38,5 +38,5 @@ async fn rss_handler(
pub async fn create(_ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> { pub async fn create(_ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
let router = Router::<Arc<dyn AppContextTrait>>::new().route("/rss/{token}", get(rss_handler)); let router = Router::<Arc<dyn AppContextTrait>>::new().route("/rss/{token}", get(rss_handler));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
} }

View File

@@ -71,5 +71,5 @@ pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller>
post(graphql_handler).layer(from_fn_with_state(ctx, auth_middleware)), post(graphql_handler).layer(from_fn_with_state(ctx, auth_middleware)),
) )
.route("/introspection", introspection_handler); .route("/introspection", introspection_handler);
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
} }

View File

@@ -38,5 +38,5 @@ pub async fn create(_context: Arc<dyn AppContextTrait>) -> RecorderResult<Contro
.route("/health", get(health)) .route("/health", get(health))
.route("/ping", get(ping)); .route("/ping", get(ping));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
} }

View File

@@ -5,4 +5,4 @@ pub mod metadata;
pub mod oidc; pub mod oidc;
pub mod r#static; pub mod r#static;
pub use core::{Controller, ControllerTrait, PrefixController}; pub use core::{Controller, ControllerTrait, NestRouterController};

View File

@@ -77,5 +77,5 @@ pub async fn create(_context: Arc<dyn AppContextTrait>) -> RecorderResult<Contro
.route("/auth", get(oidc_auth)) .route("/auth", get(oidc_auth))
.route("/callback", get(oidc_callback)); .route("/callback", get(oidc_callback));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
} }

View File

@@ -99,5 +99,5 @@ pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller>
) )
.route("/public/{*path}", get(serve_public_static)); .route("/public/{*path}", get(serve_public_static));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
} }

View File

@@ -1,8 +0,0 @@
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@@ -0,0 +1,8 @@
AUTH__AUTH_TYPE = "basic" # or oidc
AUTH__BASIC_USER = "konobangu"
AUTH__BASIC_PASSWORD = "konobangu"
# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# AUTH__OIDC_CLIENT_ID = "client_id"
# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@@ -0,0 +1,6 @@
AUTH__AUTH_TYPE = "basic" # or oidc
# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# AUTH__OIDC_CLIENT_ID = "client_id"
# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@@ -49,6 +49,7 @@
"@rsbuild/plugin-react": "^1.3.2", "@rsbuild/plugin-react": "^1.3.2",
"@tanstack/react-form": "^1.12.3", "@tanstack/react-form": "^1.12.3",
"@tanstack/react-query": "^5.80.7", "@tanstack/react-query": "^5.80.7",
"@tanstack/react-router": "^1.121.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tanstack/store": "^0.7.1", "@tanstack/store": "^0.7.1",
"arktype": "^2.1.20", "arktype": "^2.1.20",
@@ -76,8 +77,7 @@
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4", "tw-animate-css": "^1.3.4",
"type-fest": "^4.41.0", "type-fest": "^4.41.0",
"vaul": "^1.1.2", "vaul": "^1.1.2"
"@tanstack/react-router": "^1.121.2"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/cli": "^5.0.7",
@@ -86,13 +86,14 @@
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.2.0",
"@parcel/watcher": "^2.5.1", "@parcel/watcher": "^2.5.1",
"@rsbuild/core": "^1.3.22", "@rsbuild/core": "^1.3.22",
"@rsbuild/plugin-type-check": "^1.2.3",
"@tailwindcss/postcss": "^4.1.10", "@tailwindcss/postcss": "^4.1.10",
"@tanstack/router-devtools": "^1.121.5",
"@tanstack/router-plugin": "^1.121.4",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"commander": "^14.0.0", "commander": "^14.0.0",
"postcss": "^8.5.5", "postcss": "^8.5.5"
"@tanstack/router-devtools": "^1.121.5",
"@tanstack/router-plugin": "^1.121.4"
} }
} }

View File

@@ -1,13 +1,26 @@
import { defineConfig } from '@rsbuild/core'; import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react'; import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'; import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack';
const TS_NO_CHECK_REGEX =
/[\\/]node_modules[\\/]|[\\/]gql[\\/]|[\\/]components[\\/]ui[\\/]/;
export default defineConfig({ export default defineConfig({
html: { html: {
title: 'Konobangu', title: 'Konobangu',
favicon: './public/assets/favicon.ico', favicon: './public/assets/favicon.ico',
}, },
plugins: [pluginReact()], plugins: [
pluginReact(),
pluginTypeCheck({
tsCheckerOptions: {
issue: {
exclude: [({ file = '' }) => TS_NO_CHECK_REGEX.test(file)],
},
},
}),
],
tools: { tools: {
rspack: { rspack: {
plugins: [ plugins: [
@@ -20,15 +33,23 @@ export default defineConfig({
index: './src/main.tsx', index: './src/main.tsx',
}, },
define: { define: {
'process.env.AUTH_TYPE': JSON.stringify(process.env.AUTH_TYPE), 'process.env.AUTH__AUTH_TYPE': JSON.stringify(
'process.env.OIDC_CLIENT_ID': JSON.stringify(process.env.OIDC_CLIENT_ID), process.env.AUTH__AUTH_TYPE
'process.env.OIDC_CLIENT_SECRET': JSON.stringify(
process.env.OIDC_CLIENT_SECRET
), ),
'process.env.OIDC_ISSUER': JSON.stringify(process.env.OIDC_ISSUER), 'process.env.AUTH__OIDC_CLIENT_ID': JSON.stringify(
'process.env.OIDC_AUDIENCE': JSON.stringify(process.env.OIDC_AUDIENCE), process.env.AUTH__OIDC_CLIENT_ID
'process.env.OIDC_EXTRA_SCOPES': JSON.stringify( ),
process.env.OIDC_EXTRA_SCOPES 'process.env.AUTH__OIDC_CLIENT_SECRET': JSON.stringify(
process.env.AUTH__OIDC_CLIENT_SECRET
),
'process.env.AUTH__OIDC_ISSUER': JSON.stringify(
process.env.AUTH__OIDC_ISSUER
),
'process.env.AUTH__OIDC_AUDIENCE': JSON.stringify(
process.env.AUTH__OIDC_AUDIENCE
),
'process.env.AUTH__OIDC_EXTRA_SCOPES': JSON.stringify(
process.env.AUTH__OIDC_EXTRA_SCOPES
), ),
}, },
}, },
@@ -39,7 +60,7 @@ export default defineConfig({
setupMiddlewares: [ setupMiddlewares: [
(middlewares) => { (middlewares) => {
middlewares.unshift((req, res, next) => { middlewares.unshift((req, res, next) => {
if (process.env.AUTH_TYPE === 'basic') { if (process.env.AUTH__AUTH_TYPE === 'basic') {
res.setHeader('WWW-Authenticate', 'Basic realm="konobangu"'); res.setHeader('WWW-Authenticate', 'Basic realm="konobangu"');
const authorization = const authorization =
@@ -49,8 +70,8 @@ export default defineConfig({
.split(':'); .split(':');
if ( if (
user !== process.env.BASIC_USER || user !== process.env.AUTH__BASIC_USER ||
password !== process.env.BASIC_PASSWORD password !== process.env.AUTH__BASIC_PASSWORD
) { ) {
res.statusCode = 401; res.statusCode = 401;
res.write('Unauthorized'); res.write('Unauthorized');

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { MoreHorizontal } from "lucide-react"; import { Eye, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -18,7 +18,7 @@ import { ComponentProps, PropsWithChildren } from "react";
interface DropdownMenuActionsProps<Id> interface DropdownMenuActionsProps<Id>
extends ComponentProps<typeof DropdownMenuPrimitive.Root> { extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
id: Id; id: Id;
showDetail?: boolean; showDetail?: boolean | "dropdown-menu";
showEdit?: boolean; showEdit?: boolean;
showDelete?: boolean; showDelete?: boolean;
onDetail?: (id: Id) => void; onDetail?: (id: Id) => void;
@@ -38,6 +38,17 @@ export function DropdownMenuActions<Id>({
...rest ...rest
}: PropsWithChildren<DropdownMenuActionsProps<Id>>) { }: PropsWithChildren<DropdownMenuActionsProps<Id>>) {
return ( return (
<div className="flex gap-2 items-center justify-center">
{showDetail === true && (
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
onClick={() => onDetail?.(id)}
>
<Eye />
<span className="sr-only">Detail</span>
</Button>
)}
<DropdownMenu {...rest}> <DropdownMenu {...rest}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@@ -50,15 +61,18 @@ export function DropdownMenuActions<Id>({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]"> <DropdownMenuContent align="end" className="w-[160px]">
{children} {children}
{showDetail && ( {showDetail === "dropdown-menu" && (
<DropdownMenuItem onClick={() => onDetail?.(id)}> <DropdownMenuItem onClick={() => onDetail?.(id)}>
Detail Detail
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{showEdit && ( {showEdit && (
<DropdownMenuItem onClick={() => onEdit?.(id)}>Edit</DropdownMenuItem> <DropdownMenuItem onClick={() => onEdit?.(id)}>
Edit
</DropdownMenuItem>
)} )}
{(showDetail || showEdit) && showDelete && <DropdownMenuSeparator />} {(showDetail === "dropdown-menu" || showEdit || children) &&
showDelete && <DropdownMenuSeparator />}
{showDelete && ( {showDelete && (
<DropdownMenuItem onClick={() => onDelete?.(id)}> <DropdownMenuItem onClick={() => onDelete?.(id)}>
Delete Delete
@@ -67,5 +81,6 @@ export function DropdownMenuActions<Id>({
)} )}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
); );
} }

View File

@@ -7,8 +7,8 @@ import { gql } from '@apollo/client';
import { type } from 'arktype'; import { type } from 'arktype';
export const GET_CREDENTIAL_3RD = gql` export const GET_CREDENTIAL_3RD = gql`
query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) { query GetCredential3rd($filter: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {
credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) { credential3rd(filter: $filter, orderBy: $orderBy, pagination: $pagination) {
nodes { nodes {
id id
cookies cookies
@@ -43,8 +43,8 @@ export const INSERT_CREDENTIAL_3RD = gql`
`; `;
export const UPDATE_CREDENTIAL_3RD = gql` export const UPDATE_CREDENTIAL_3RD = gql`
mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) { mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filter: Credential3rdFilterInput!) {
credential3rdUpdate(data: $data, filter: $filters) { credential3rdUpdate(data: $data, filter: $filter) {
id id
cookies cookies
username username
@@ -58,14 +58,14 @@ export const UPDATE_CREDENTIAL_3RD = gql`
`; `;
export const DELETE_CREDENTIAL_3RD = gql` export const DELETE_CREDENTIAL_3RD = gql`
mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) { mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {
credential3rdDelete(filter: $filters) credential3rdDelete(filter: $filter)
} }
`; `;
export const GET_CREDENTIAL_3RD_DETAIL = gql` export const GET_CREDENTIAL_3RD_DETAIL = gql`
query GetCredential3rdDetail($id: Int!) { query GetCredential3rdDetail($id: Int!) {
credential3rd(filters: { id: { eq: $id } }) { credential3rd(filter: { id: { eq: $id } }) {
nodes { nodes {
id id
cookies cookies
@@ -81,8 +81,8 @@ export const GET_CREDENTIAL_3RD_DETAIL = gql`
`; `;
export const CHECK_CREDENTIAL_3RD_AVAILABLE = gql` export const CHECK_CREDENTIAL_3RD_AVAILABLE = gql`
query CheckCredential3rdAvailable($id: Int!) { mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {
credential3rdCheckAvailable(filter: { id: $id }) { credential3rdCheckAvailable(filter: $filter) {
available available
} }
} }

View File

@@ -13,7 +13,7 @@ export const INSERT_FEED = gql`
`; `;
export const DELETE_FEED = gql` export const DELETE_FEED = gql`
mutation DeleteFeed($filters: FeedsFilterInput!) { mutation DeleteFeed($filter: FeedsFilterInput!) {
feedsDelete(filter: $filters) feedsDelete(filter: $filter)
} }
`; `;

View File

@@ -14,10 +14,10 @@ import {
} from './mikan'; } from './mikan';
export const GET_SUBSCRIPTIONS = gql` export const GET_SUBSCRIPTIONS = gql`
query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) { query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {
subscriptions( subscriptions(
pagination: $pagination pagination: $pagination
filters: $filters filter: $filter
orderBy: $orderBy orderBy: $orderBy
) { ) {
nodes { nodes {
@@ -59,11 +59,11 @@ export type SubscriptionDto =
export const UPDATE_SUBSCRIPTIONS = gql` export const UPDATE_SUBSCRIPTIONS = gql`
mutation UpdateSubscriptions( mutation UpdateSubscriptions(
$data: SubscriptionsUpdateInput!, $data: SubscriptionsUpdateInput!,
$filters: SubscriptionsFilterInput!, $filter: SubscriptionsFilterInput!,
) { ) {
subscriptionsUpdate ( subscriptionsUpdate (
data: $data data: $data
filter: $filters filter: $filter
) { ) {
id id
createdAt createdAt
@@ -77,14 +77,14 @@ export const UPDATE_SUBSCRIPTIONS = gql`
`; `;
export const DELETE_SUBSCRIPTIONS = gql` export const DELETE_SUBSCRIPTIONS = gql`
mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) { mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {
subscriptionsDelete(filter: $filters) subscriptionsDelete(filter: $filter)
} }
`; `;
export const GET_SUBSCRIPTION_DETAIL = gql` export const GET_SUBSCRIPTION_DETAIL = gql`
query GetSubscriptionDetail ($id: Int!) { query GetSubscriptionDetail ($id: Int!) {
subscriptions(filters: { id: { subscriptions(filter: { id: {
eq: $id eq: $id
} }) { } }) {
nodes { nodes {
@@ -105,6 +105,13 @@ query GetSubscriptionDetail ($id: Int!) {
feedSource feedSource
} }
} }
subscriberTask {
nodes {
id
taskType
status
}
}
credential3rd { credential3rd {
id id
username username

View File

@@ -7,10 +7,10 @@ import { type } from 'arktype';
import { SubscriptionSchema } from './subscriptions'; import { SubscriptionSchema } from './subscriptions';
export const GET_TASKS = gql` export const GET_TASKS = gql`
query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) { query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {
subscriberTasks( subscriberTasks(
pagination: $pagination pagination: $pagination
filters: $filters filter: $filter
orderBy: $orderBy orderBy: $orderBy
) { ) {
nodes { nodes {
@@ -36,14 +36,14 @@ export const GET_TASKS = gql`
`; `;
export const DELETE_TASKS = gql` export const DELETE_TASKS = gql`
mutation DeleteTasks($filters: SubscriberTasksFilterInput!) { mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {
subscriberTasksDelete(filter: $filters) subscriberTasksDelete(filter: $filter)
} }
`; `;
export const RETRY_TASKS = gql` export const RETRY_TASKS = gql`
mutation RetryTasks($filters: SubscriberTasksFilterInput!) { mutation RetryTasks($filter: SubscriberTasksFilterInput!) {
subscriberTasksRetryOne(filter: $filters) { subscriberTasksRetryOne(filter: $filter) {
id, id,
job, job,
taskType, taskType,

View File

@@ -8,5 +8,5 @@ export const AUTH_METHOD = {
export type AuthMethodType = ValueOf<typeof AUTH_METHOD>; export type AuthMethodType = ValueOf<typeof AUTH_METHOD>;
export function getAppAuthMethod(): AuthMethodType { export function getAppAuthMethod(): AuthMethodType {
return process.env.AUTH_TYPE as AuthMethodType; return process.env.AUTH__AUTH_TYPE as AuthMethodType;
} }

View File

@@ -3,16 +3,16 @@ import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx';
export function buildOidcConfig(): OpenIdConfiguration { export function buildOidcConfig(): OpenIdConfiguration {
const origin = window.location.origin; const origin = window.location.origin;
const resource = process.env.OIDC_AUDIENCE!; const resource = process.env.AUTH__OIDC_AUDIENCE!;
return { return {
authority: process.env.OIDC_ISSUER!, authority: process.env.AUTH__OIDC_ISSUER!,
redirectUrl: `${origin}/auth/oidc/callback`, redirectUrl: `${origin}/auth/oidc/callback`,
postLogoutRedirectUri: `${origin}/`, postLogoutRedirectUri: `${origin}/`,
clientId: process.env.OIDC_CLIENT_ID!, clientId: process.env.AUTH__OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET, clientSecret: process.env.AUTH__OIDC_CLIENT_SECRET,
scope: process.env.OIDC_EXTRA_SCOPES scope: process.env.AUTH__OIDC_EXTRA_SCOPES
? `openid profile email offline_access ${process.env.OIDC_EXTRA_SCOPES}` ? `openid profile email offline_access ${process.env.AUTH__OIDC_EXTRA_SCOPES}`
: 'openid profile email offline_access', : 'openid profile email offline_access',
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
responseType: 'code', responseType: 'code',

View File

@@ -14,46 +14,46 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/ */
type Documents = { type Documents = {
"\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetCredential3rdDocument, "\n query GetCredential3rd($filter: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filter: $filter, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetCredential3rdDocument,
"\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.InsertCredential3rdDocument, "\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.InsertCredential3rdDocument,
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.UpdateCredential3rdDocument, "\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filter: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filter) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": typeof types.DeleteCredential3rdDocument, "\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": typeof types.DeleteCredential3rdDocument,
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument, "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
"\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument, "\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument,
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument, "\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument,
"\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n": typeof types.DeleteFeedDocument, "\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": typeof types.DeleteFeedDocument,
"\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument, "\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
"\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 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 $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\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($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { 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 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, "\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, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\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 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 mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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, "\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,
"\n mutation DeleteTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filters)\n }\n": typeof types.DeleteTasksDocument, "\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
"\n mutation RetryTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filters) {\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 }\n": typeof types.RetryTasksDocument, "\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\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 }\n": typeof types.RetryTasksDocument,
}; };
const documents: Documents = { const documents: Documents = {
"\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument, "\n query GetCredential3rd($filter: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filter: $filter, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument,
"\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.InsertCredential3rdDocument, "\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.InsertCredential3rdDocument,
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.UpdateCredential3rdDocument, "\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filter: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filter) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": types.DeleteCredential3rdDocument, "\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": types.DeleteCredential3rdDocument,
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument, "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
"\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument, "\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument,
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument, "\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument,
"\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n": types.DeleteFeedDocument, "\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": types.DeleteFeedDocument,
"\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument, "\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
"\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 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 $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\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($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { 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 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, "\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, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\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 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 mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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, "\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,
"\n mutation DeleteTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filters)\n }\n": types.DeleteTasksDocument, "\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
"\n mutation RetryTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filters) {\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 }\n": types.RetryTasksDocument, "\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\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 }\n": types.RetryTasksDocument,
}; };
/** /**
@@ -73,7 +73,7 @@ export function gql(source: string): unknown;
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"]; export function gql(source: "\n query GetCredential3rd($filter: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filter: $filter, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetCredential3rd($filter: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filter: $filter, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -81,19 +81,19 @@ export function gql(source: "\n mutation InsertCredential3rd($data: Credential3
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n"): (typeof documents)["\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n"]; export function gql(source: "\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filter: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filter) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n"): (typeof documents)["\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filter: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filter) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n"]; export function gql(source: "\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"): (typeof documents)["\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"]; export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"): (typeof documents)["\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n"): (typeof documents)["\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n"]; export function gql(source: "\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n"): (typeof documents)["\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -101,11 +101,11 @@ export function gql(source: "\n mutation InsertFeed($data: FeedsInsertInput!)
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n"]; export function gql(source: "\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"]; export function gql(source: "\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -113,15 +113,15 @@ export function gql(source: "\n mutation InsertSubscription($data: Subscripti
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n"): (typeof documents)["\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n"]; export function gql(source: "\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 documents)["\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"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n"]; export function gql(source: "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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(filters: { 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 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(filters: { 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 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 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"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -137,15 +137,15 @@ export function gql(source: "\n mutation SyncSubscriptionSources($filter: Subsc
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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 documents)["\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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"]; export function gql(source: "\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 documents)["\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"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 DeleteTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filters)\n }\n"]; export function gql(source: "\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * 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 RetryTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filters) {\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 }\n"): (typeof documents)["\n mutation RetryTasks($filters: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filters) {\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 }\n"]; export function gql(source: "\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\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 }\n"): (typeof documents)["\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\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 }\n"];
export function gql(source: string) { export function gql(source: string) {
return (documents as any)[source] ?? {}; return (documents as any)[source] ?? {};

View File

@@ -46,21 +46,21 @@ export type Bangumi = {
export type BangumiEpisodeArgs = { export type BangumiEpisodeArgs = {
filters?: InputMaybe<EpisodesFilterInput>; filter?: InputMaybe<EpisodesFilterInput>;
orderBy?: InputMaybe<EpisodesOrderInput>; orderBy?: InputMaybe<EpisodesOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type BangumiSubscriptionArgs = { export type BangumiSubscriptionArgs = {
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
orderBy?: InputMaybe<SubscriptionsOrderInput>; orderBy?: InputMaybe<SubscriptionsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type BangumiSubscriptionBangumiArgs = { export type BangumiSubscriptionBangumiArgs = {
filters?: InputMaybe<SubscriptionBangumiFilterInput>; filter?: InputMaybe<SubscriptionBangumiFilterInput>;
orderBy?: InputMaybe<SubscriptionBangumiOrderInput>; orderBy?: InputMaybe<SubscriptionBangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -228,7 +228,7 @@ export type Credential3rd = {
export type Credential3rdSubscriptionArgs = { export type Credential3rdSubscriptionArgs = {
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
orderBy?: InputMaybe<SubscriptionsOrderInput>; orderBy?: InputMaybe<SubscriptionsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -252,11 +252,6 @@ export type Credential3rdCheckAvailableInfo = {
available: Scalars['Boolean']['output']; available: Scalars['Boolean']['output'];
}; };
/** The input of the credential3rdCheckAvailable query */
export type Credential3rdCheckAvailableInput = {
id: Scalars['Int']['input'];
};
export type Credential3rdConnection = { export type Credential3rdConnection = {
__typename?: 'Credential3rdConnection'; __typename?: 'Credential3rdConnection';
edges: Array<Credential3rdEdge>; edges: Array<Credential3rdEdge>;
@@ -338,6 +333,148 @@ export type Credential3rdUpdateInput = {
username?: InputMaybe<Scalars['String']['input']>; username?: InputMaybe<Scalars['String']['input']>;
}; };
export type Cron = {
__typename?: 'Cron';
attempts: Scalars['Int']['output'];
createdAt: Scalars['String']['output'];
cronExpr: Scalars['String']['output'];
enabled: Scalars['Boolean']['output'];
id: Scalars['Int']['output'];
lastError?: Maybe<Scalars['String']['output']>;
lastRun?: Maybe<Scalars['String']['output']>;
lockedAt?: Maybe<Scalars['String']['output']>;
lockedBy?: Maybe<Scalars['String']['output']>;
maxAttempts: Scalars['Int']['output'];
nextRun?: Maybe<Scalars['String']['output']>;
priority: Scalars['Int']['output'];
status: CronStatusEnum;
subscriber?: Maybe<Subscribers>;
subscriberId?: Maybe<Scalars['Int']['output']>;
subscriberTask?: Maybe<Scalars['Json']['output']>;
subscription?: Maybe<Subscriptions>;
subscriptionId?: Maybe<Scalars['Int']['output']>;
timeoutMs: Scalars['Int']['output'];
updatedAt: Scalars['String']['output'];
};
export type CronBasic = {
__typename?: 'CronBasic';
attempts: Scalars['Int']['output'];
createdAt: Scalars['String']['output'];
cronExpr: Scalars['String']['output'];
enabled: Scalars['Boolean']['output'];
id: Scalars['Int']['output'];
lastError?: Maybe<Scalars['String']['output']>;
lastRun?: Maybe<Scalars['String']['output']>;
lockedAt?: Maybe<Scalars['String']['output']>;
lockedBy?: Maybe<Scalars['String']['output']>;
maxAttempts: Scalars['Int']['output'];
nextRun?: Maybe<Scalars['String']['output']>;
priority: Scalars['Int']['output'];
status: CronStatusEnum;
subscriberId?: Maybe<Scalars['Int']['output']>;
subscriberTask?: Maybe<Scalars['Json']['output']>;
subscriptionId?: Maybe<Scalars['Int']['output']>;
timeoutMs: Scalars['Int']['output'];
updatedAt: Scalars['String']['output'];
};
export type CronConnection = {
__typename?: 'CronConnection';
edges: Array<CronEdge>;
nodes: Array<Cron>;
pageInfo: PageInfo;
paginationInfo?: Maybe<PaginationInfo>;
};
export type CronEdge = {
__typename?: 'CronEdge';
cursor: Scalars['String']['output'];
node: Cron;
};
export type CronFilterInput = {
and?: InputMaybe<Array<CronFilterInput>>;
attempts?: InputMaybe<IntegerFilterInput>;
createdAt?: InputMaybe<TextFilterInput>;
cronExpr?: InputMaybe<StringFilterInput>;
enabled?: InputMaybe<BooleanFilterInput>;
id?: InputMaybe<IntegerFilterInput>;
lastError?: InputMaybe<StringFilterInput>;
lastRun?: InputMaybe<TextFilterInput>;
lockedAt?: InputMaybe<TextFilterInput>;
lockedBy?: InputMaybe<StringFilterInput>;
maxAttempts?: InputMaybe<IntegerFilterInput>;
nextRun?: InputMaybe<TextFilterInput>;
or?: InputMaybe<Array<CronFilterInput>>;
priority?: InputMaybe<IntegerFilterInput>;
status?: InputMaybe<CronStatusEnumFilterInput>;
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
subscriberTask?: InputMaybe<Scalars['JsonbFilterInput']['input']>;
subscriptionId?: InputMaybe<IntegerFilterInput>;
timeoutMs?: InputMaybe<IntegerFilterInput>;
updatedAt?: InputMaybe<TextFilterInput>;
};
export type CronInsertInput = {
cronExpr: Scalars['String']['input'];
enabled?: InputMaybe<Scalars['Boolean']['input']>;
id?: InputMaybe<Scalars['Int']['input']>;
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
subscriberTask?: InputMaybe<Scalars['Json']['input']>;
timeoutMs?: InputMaybe<Scalars['Int']['input']>;
};
export type CronOrderInput = {
attempts?: InputMaybe<OrderByEnum>;
createdAt?: InputMaybe<OrderByEnum>;
cronExpr?: InputMaybe<OrderByEnum>;
enabled?: InputMaybe<OrderByEnum>;
id?: InputMaybe<OrderByEnum>;
lastError?: InputMaybe<OrderByEnum>;
lastRun?: InputMaybe<OrderByEnum>;
lockedAt?: InputMaybe<OrderByEnum>;
lockedBy?: InputMaybe<OrderByEnum>;
maxAttempts?: InputMaybe<OrderByEnum>;
nextRun?: InputMaybe<OrderByEnum>;
priority?: InputMaybe<OrderByEnum>;
status?: InputMaybe<OrderByEnum>;
subscriberId?: InputMaybe<OrderByEnum>;
subscriberTask?: InputMaybe<OrderByEnum>;
subscriptionId?: InputMaybe<OrderByEnum>;
timeoutMs?: InputMaybe<OrderByEnum>;
updatedAt?: InputMaybe<OrderByEnum>;
};
export const CronStatusEnum = {
Completed: 'completed',
Failed: 'failed',
Pending: 'pending',
Running: 'running'
} as const;
export type CronStatusEnum = typeof CronStatusEnum[keyof typeof CronStatusEnum];
export type CronStatusEnumFilterInput = {
eq?: InputMaybe<CronStatusEnum>;
gt?: InputMaybe<CronStatusEnum>;
gte?: InputMaybe<CronStatusEnum>;
is_in?: InputMaybe<Array<CronStatusEnum>>;
is_not_in?: InputMaybe<Array<CronStatusEnum>>;
is_not_null?: InputMaybe<CronStatusEnum>;
is_null?: InputMaybe<CronStatusEnum>;
lt?: InputMaybe<CronStatusEnum>;
lte?: InputMaybe<CronStatusEnum>;
ne?: InputMaybe<CronStatusEnum>;
};
export type CronUpdateInput = {
cronExpr?: InputMaybe<Scalars['String']['input']>;
enabled?: InputMaybe<Scalars['Boolean']['input']>;
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
priority?: InputMaybe<Scalars['Int']['input']>;
timeoutMs?: InputMaybe<Scalars['Int']['input']>;
};
export type CursorInput = { export type CursorInput = {
cursor?: InputMaybe<Scalars['String']['input']>; cursor?: InputMaybe<Scalars['String']['input']>;
limit: Scalars['Int']['input']; limit: Scalars['Int']['input'];
@@ -421,7 +558,7 @@ export type Downloaders = {
export type DownloadersDownloadArgs = { export type DownloadersDownloadArgs = {
filters?: InputMaybe<DownloadsFilterInput>; filter?: InputMaybe<DownloadsFilterInput>;
orderBy?: InputMaybe<DownloadsOrderInput>; orderBy?: InputMaybe<DownloadsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -682,21 +819,21 @@ export type Episodes = {
export type EpisodesDownloadArgs = { export type EpisodesDownloadArgs = {
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
orderBy?: InputMaybe<SubscriptionsOrderInput>; orderBy?: InputMaybe<SubscriptionsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type EpisodesSubscriptionArgs = { export type EpisodesSubscriptionArgs = {
filters?: InputMaybe<DownloadsFilterInput>; filter?: InputMaybe<DownloadsFilterInput>;
orderBy?: InputMaybe<DownloadsOrderInput>; orderBy?: InputMaybe<DownloadsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type EpisodesSubscriptionEpisodeArgs = { export type EpisodesSubscriptionEpisodeArgs = {
filters?: InputMaybe<SubscriptionEpisodeFilterInput>; filter?: InputMaybe<SubscriptionEpisodeFilterInput>;
orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>; orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -990,10 +1127,15 @@ export type Mutation = {
bangumiCreateOne: BangumiBasic; bangumiCreateOne: BangumiBasic;
bangumiDelete: Scalars['Int']['output']; bangumiDelete: Scalars['Int']['output'];
bangumiUpdate: Array<BangumiBasic>; bangumiUpdate: Array<BangumiBasic>;
credential3rdCheckAvailable: Credential3rdCheckAvailableInfo;
credential3rdCreateBatch: Array<Credential3rdBasic>; credential3rdCreateBatch: Array<Credential3rdBasic>;
credential3rdCreateOne: Credential3rdBasic; credential3rdCreateOne: Credential3rdBasic;
credential3rdDelete: Scalars['Int']['output']; credential3rdDelete: Scalars['Int']['output'];
credential3rdUpdate: Array<Credential3rdBasic>; credential3rdUpdate: Array<Credential3rdBasic>;
cronCreateBatch: Array<CronBasic>;
cronCreateOne: CronBasic;
cronDelete: Scalars['Int']['output'];
cronUpdate: Array<CronBasic>;
downloadersCreateBatch: Array<DownloadersBasic>; downloadersCreateBatch: Array<DownloadersBasic>;
downloadersCreateOne: DownloadersBasic; downloadersCreateOne: DownloadersBasic;
downloadersDelete: Scalars['Int']['output']; downloadersDelete: Scalars['Int']['output'];
@@ -1010,8 +1152,9 @@ export type Mutation = {
feedsCreateOne: FeedsBasic; feedsCreateOne: FeedsBasic;
feedsDelete: Scalars['Int']['output']; feedsDelete: Scalars['Int']['output'];
feedsUpdate: Array<FeedsBasic>; feedsUpdate: Array<FeedsBasic>;
subscriberTasksCreateOne: SubscriberTasksBasic;
subscriberTasksDelete: Scalars['Int']['output']; subscriberTasksDelete: Scalars['Int']['output'];
subscriberTasksRetryOne: SubscriberTasks; subscriberTasksRetryOne: SubscriberTasksBasic;
subscriptionBangumiCreateBatch: Array<SubscriptionBangumiBasic>; subscriptionBangumiCreateBatch: Array<SubscriptionBangumiBasic>;
subscriptionBangumiCreateOne: SubscriptionBangumiBasic; subscriptionBangumiCreateOne: SubscriptionBangumiBasic;
subscriptionBangumiDelete: Scalars['Int']['output']; subscriptionBangumiDelete: Scalars['Int']['output'];
@@ -1023,9 +1166,9 @@ export type Mutation = {
subscriptionsCreateBatch: Array<SubscriptionsBasic>; subscriptionsCreateBatch: Array<SubscriptionsBasic>;
subscriptionsCreateOne: SubscriptionsBasic; subscriptionsCreateOne: SubscriptionsBasic;
subscriptionsDelete: Scalars['Int']['output']; subscriptionsDelete: Scalars['Int']['output'];
subscriptionsSyncOneFeedsFull: SubscriberTasks; subscriptionsSyncOneFeedsFull: SubscriberTasksBasic;
subscriptionsSyncOneFeedsIncremental: SubscriberTasks; subscriptionsSyncOneFeedsIncremental: SubscriberTasksBasic;
subscriptionsSyncOneSources: SubscriberTasks; subscriptionsSyncOneSources: SubscriberTasksBasic;
subscriptionsUpdate: Array<SubscriptionsBasic>; subscriptionsUpdate: Array<SubscriptionsBasic>;
}; };
@@ -1051,6 +1194,11 @@ export type MutationBangumiUpdateArgs = {
}; };
export type MutationCredential3rdCheckAvailableArgs = {
filter?: InputMaybe<Credential3rdFilterInput>;
};
export type MutationCredential3rdCreateBatchArgs = { export type MutationCredential3rdCreateBatchArgs = {
data: Array<Credential3rdInsertInput>; data: Array<Credential3rdInsertInput>;
}; };
@@ -1072,6 +1220,27 @@ export type MutationCredential3rdUpdateArgs = {
}; };
export type MutationCronCreateBatchArgs = {
data: Array<CronInsertInput>;
};
export type MutationCronCreateOneArgs = {
data: CronInsertInput;
};
export type MutationCronDeleteArgs = {
filter?: InputMaybe<CronFilterInput>;
};
export type MutationCronUpdateArgs = {
data: CronUpdateInput;
filter?: InputMaybe<CronFilterInput>;
};
export type MutationDownloadersCreateBatchArgs = { export type MutationDownloadersCreateBatchArgs = {
data: Array<DownloadersInsertInput>; data: Array<DownloadersInsertInput>;
}; };
@@ -1156,6 +1325,11 @@ export type MutationFeedsUpdateArgs = {
}; };
export type MutationSubscriberTasksCreateOneArgs = {
data: SubscriberTasksInsertInput;
};
export type MutationSubscriberTasksDeleteArgs = { export type MutationSubscriberTasksDeleteArgs = {
filter?: InputMaybe<SubscriberTasksFilterInput>; filter?: InputMaybe<SubscriberTasksFilterInput>;
}; };
@@ -1286,7 +1460,7 @@ export type Query = {
_sea_orm_entity_metadata?: Maybe<Scalars['String']['output']>; _sea_orm_entity_metadata?: Maybe<Scalars['String']['output']>;
bangumi: BangumiConnection; bangumi: BangumiConnection;
credential3rd: Credential3rdConnection; credential3rd: Credential3rdConnection;
credential3rdCheckAvailable: Credential3rdCheckAvailableInfo; cron: CronConnection;
downloaders: DownloadersConnection; downloaders: DownloadersConnection;
downloads: DownloadsConnection; downloads: DownloadsConnection;
episodes: EpisodesConnection; episodes: EpisodesConnection;
@@ -1305,82 +1479,84 @@ export type Query_Sea_Orm_Entity_MetadataArgs = {
export type QueryBangumiArgs = { export type QueryBangumiArgs = {
filters?: InputMaybe<BangumiFilterInput>; filter?: InputMaybe<BangumiFilterInput>;
orderBy?: InputMaybe<BangumiOrderInput>; orderBy?: InputMaybe<BangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryCredential3rdArgs = { export type QueryCredential3rdArgs = {
filters?: InputMaybe<Credential3rdFilterInput>; filter?: InputMaybe<Credential3rdFilterInput>;
orderBy?: InputMaybe<Credential3rdOrderInput>; orderBy?: InputMaybe<Credential3rdOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryCredential3rdCheckAvailableArgs = { export type QueryCronArgs = {
filter: Credential3rdCheckAvailableInput; filter?: InputMaybe<CronFilterInput>;
orderBy?: InputMaybe<CronOrderInput>;
pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryDownloadersArgs = { export type QueryDownloadersArgs = {
filters?: InputMaybe<DownloadersFilterInput>; filter?: InputMaybe<DownloadersFilterInput>;
orderBy?: InputMaybe<DownloadersOrderInput>; orderBy?: InputMaybe<DownloadersOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryDownloadsArgs = { export type QueryDownloadsArgs = {
filters?: InputMaybe<DownloadsFilterInput>; filter?: InputMaybe<DownloadsFilterInput>;
orderBy?: InputMaybe<DownloadsOrderInput>; orderBy?: InputMaybe<DownloadsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryEpisodesArgs = { export type QueryEpisodesArgs = {
filters?: InputMaybe<EpisodesFilterInput>; filter?: InputMaybe<EpisodesFilterInput>;
orderBy?: InputMaybe<EpisodesOrderInput>; orderBy?: InputMaybe<EpisodesOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QueryFeedsArgs = { export type QueryFeedsArgs = {
filters?: InputMaybe<FeedsFilterInput>; filter?: InputMaybe<FeedsFilterInput>;
orderBy?: InputMaybe<FeedsOrderInput>; orderBy?: InputMaybe<FeedsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QuerySubscriberTasksArgs = { export type QuerySubscriberTasksArgs = {
filters?: InputMaybe<SubscriberTasksFilterInput>; filter?: InputMaybe<SubscriberTasksFilterInput>;
orderBy?: InputMaybe<SubscriberTasksOrderInput>; orderBy?: InputMaybe<SubscriberTasksOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QuerySubscribersArgs = { export type QuerySubscribersArgs = {
filters?: InputMaybe<SubscribersFilterInput>; filter?: InputMaybe<SubscribersFilterInput>;
orderBy?: InputMaybe<SubscribersOrderInput>; orderBy?: InputMaybe<SubscribersOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QuerySubscriptionBangumiArgs = { export type QuerySubscriptionBangumiArgs = {
filters?: InputMaybe<SubscriptionBangumiFilterInput>; filter?: InputMaybe<SubscriptionBangumiFilterInput>;
orderBy?: InputMaybe<SubscriptionBangumiOrderInput>; orderBy?: InputMaybe<SubscriptionBangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QuerySubscriptionEpisodeArgs = { export type QuerySubscriptionEpisodeArgs = {
filters?: InputMaybe<SubscriptionEpisodeFilterInput>; filter?: InputMaybe<SubscriptionEpisodeFilterInput>;
orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>; orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type QuerySubscriptionsArgs = { export type QuerySubscriptionsArgs = {
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
orderBy?: InputMaybe<SubscriptionsOrderInput>; orderBy?: InputMaybe<SubscriptionsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -1441,6 +1617,26 @@ export type SubscriberTasks = {
status: SubscriberTaskStatusEnum; status: SubscriberTaskStatusEnum;
subscriber?: Maybe<Subscribers>; subscriber?: Maybe<Subscribers>;
subscriberId: Scalars['Int']['output']; subscriberId: Scalars['Int']['output'];
subscription?: Maybe<Subscriptions>;
subscriptionId?: Maybe<Scalars['Int']['output']>;
taskType: SubscriberTaskTypeEnum;
};
export type SubscriberTasksBasic = {
__typename?: 'SubscriberTasksBasic';
attempts: Scalars['Int']['output'];
doneAt?: Maybe<Scalars['String']['output']>;
id: Scalars['String']['output'];
job: Scalars['Json']['output'];
lastError?: Maybe<Scalars['String']['output']>;
lockAt?: Maybe<Scalars['String']['output']>;
lockBy?: Maybe<Scalars['String']['output']>;
maxAttempts: Scalars['Int']['output'];
priority: Scalars['Int']['output'];
runAt: Scalars['String']['output'];
status: SubscriberTaskStatusEnum;
subscriberId: Scalars['Int']['output'];
subscriptionId?: Maybe<Scalars['Int']['output']>;
taskType: SubscriberTaskTypeEnum; taskType: SubscriberTaskTypeEnum;
}; };
@@ -1473,9 +1669,18 @@ export type SubscriberTasksFilterInput = {
runAt?: InputMaybe<TextFilterInput>; runAt?: InputMaybe<TextFilterInput>;
status?: InputMaybe<StringFilterInput>; status?: InputMaybe<StringFilterInput>;
subscriberId?: InputMaybe<SubscriberIdFilterInput>; subscriberId?: InputMaybe<SubscriberIdFilterInput>;
subscriptionId?: InputMaybe<IntegerFilterInput>;
taskType?: InputMaybe<StringFilterInput>; taskType?: InputMaybe<StringFilterInput>;
}; };
export type SubscriberTasksInsertInput = {
id?: InputMaybe<Scalars['String']['input']>;
job: Scalars['Json']['input'];
maxAttempts: Scalars['Int']['input'];
priority: Scalars['Int']['input'];
subscriberId?: InputMaybe<Scalars['Int']['input']>;
};
export type SubscriberTasksOrderInput = { export type SubscriberTasksOrderInput = {
attempts?: InputMaybe<OrderByEnum>; attempts?: InputMaybe<OrderByEnum>;
doneAt?: InputMaybe<OrderByEnum>; doneAt?: InputMaybe<OrderByEnum>;
@@ -1489,6 +1694,7 @@ export type SubscriberTasksOrderInput = {
runAt?: InputMaybe<OrderByEnum>; runAt?: InputMaybe<OrderByEnum>;
status?: InputMaybe<OrderByEnum>; status?: InputMaybe<OrderByEnum>;
subscriberId?: InputMaybe<OrderByEnum>; subscriberId?: InputMaybe<OrderByEnum>;
subscriptionId?: InputMaybe<OrderByEnum>;
taskType?: InputMaybe<OrderByEnum>; taskType?: InputMaybe<OrderByEnum>;
}; };
@@ -1510,49 +1716,49 @@ export type Subscribers = {
export type SubscribersBangumiArgs = { export type SubscribersBangumiArgs = {
filters?: InputMaybe<BangumiFilterInput>; filter?: InputMaybe<BangumiFilterInput>;
orderBy?: InputMaybe<BangumiOrderInput>; orderBy?: InputMaybe<BangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersCredential3rdArgs = { export type SubscribersCredential3rdArgs = {
filters?: InputMaybe<Credential3rdFilterInput>; filter?: InputMaybe<Credential3rdFilterInput>;
orderBy?: InputMaybe<Credential3rdOrderInput>; orderBy?: InputMaybe<Credential3rdOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersDownloaderArgs = { export type SubscribersDownloaderArgs = {
filters?: InputMaybe<DownloadersFilterInput>; filter?: InputMaybe<DownloadersFilterInput>;
orderBy?: InputMaybe<DownloadersOrderInput>; orderBy?: InputMaybe<DownloadersOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersEpisodeArgs = { export type SubscribersEpisodeArgs = {
filters?: InputMaybe<EpisodesFilterInput>; filter?: InputMaybe<EpisodesFilterInput>;
orderBy?: InputMaybe<EpisodesOrderInput>; orderBy?: InputMaybe<EpisodesOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersFeedArgs = { export type SubscribersFeedArgs = {
filters?: InputMaybe<FeedsFilterInput>; filter?: InputMaybe<FeedsFilterInput>;
orderBy?: InputMaybe<FeedsOrderInput>; orderBy?: InputMaybe<FeedsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersSubscriberTaskArgs = { export type SubscribersSubscriberTaskArgs = {
filters?: InputMaybe<SubscriberTasksFilterInput>; filter?: InputMaybe<SubscriberTasksFilterInput>;
orderBy?: InputMaybe<SubscriberTasksOrderInput>; orderBy?: InputMaybe<SubscriberTasksOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscribersSubscriptionArgs = { export type SubscribersSubscriptionArgs = {
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
orderBy?: InputMaybe<SubscriptionsOrderInput>; orderBy?: InputMaybe<SubscriptionsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -1648,7 +1854,6 @@ export type SubscriptionBangumiUpdateInput = {
}; };
export const SubscriptionCategoryEnum = { export const SubscriptionCategoryEnum = {
Manual: 'manual',
MikanBangumi: 'mikan_bangumi', MikanBangumi: 'mikan_bangumi',
MikanSeason: 'mikan_season', MikanSeason: 'mikan_season',
MikanSubscriber: 'mikan_subscriber' MikanSubscriber: 'mikan_subscriber'
@@ -1745,6 +1950,7 @@ export type Subscriptions = {
sourceUrl: Scalars['String']['output']; sourceUrl: Scalars['String']['output'];
subscriber?: Maybe<Subscribers>; subscriber?: Maybe<Subscribers>;
subscriberId: Scalars['Int']['output']; subscriberId: Scalars['Int']['output'];
subscriberTask: SubscriberTasksConnection;
subscriptionBangumi: SubscriptionBangumiConnection; subscriptionBangumi: SubscriptionBangumiConnection;
subscriptionEpisode: SubscriptionEpisodeConnection; subscriptionEpisode: SubscriptionEpisodeConnection;
updatedAt: Scalars['String']['output']; updatedAt: Scalars['String']['output'];
@@ -1752,35 +1958,42 @@ export type Subscriptions = {
export type SubscriptionsBangumiArgs = { export type SubscriptionsBangumiArgs = {
filters?: InputMaybe<BangumiFilterInput>; filter?: InputMaybe<BangumiFilterInput>;
orderBy?: InputMaybe<BangumiOrderInput>; orderBy?: InputMaybe<BangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscriptionsEpisodeArgs = { export type SubscriptionsEpisodeArgs = {
filters?: InputMaybe<EpisodesFilterInput>; filter?: InputMaybe<EpisodesFilterInput>;
orderBy?: InputMaybe<EpisodesOrderInput>; orderBy?: InputMaybe<EpisodesOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscriptionsFeedArgs = { export type SubscriptionsFeedArgs = {
filters?: InputMaybe<FeedsFilterInput>; filter?: InputMaybe<FeedsFilterInput>;
orderBy?: InputMaybe<FeedsOrderInput>; orderBy?: InputMaybe<FeedsOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscriptionsSubscriberTaskArgs = {
filter?: InputMaybe<SubscriberTasksFilterInput>;
orderBy?: InputMaybe<SubscriberTasksOrderInput>;
pagination?: InputMaybe<PaginationInput>;
};
export type SubscriptionsSubscriptionBangumiArgs = { export type SubscriptionsSubscriptionBangumiArgs = {
filters?: InputMaybe<SubscriptionBangumiFilterInput>; filter?: InputMaybe<SubscriptionBangumiFilterInput>;
orderBy?: InputMaybe<SubscriptionBangumiOrderInput>; orderBy?: InputMaybe<SubscriptionBangumiOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
export type SubscriptionsSubscriptionEpisodeArgs = { export type SubscriptionsSubscriptionEpisodeArgs = {
filters?: InputMaybe<SubscriptionEpisodeFilterInput>; filter?: InputMaybe<SubscriptionEpisodeFilterInput>;
orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>; orderBy?: InputMaybe<SubscriptionEpisodeOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}; };
@@ -1877,7 +2090,7 @@ export type TextFilterInput = {
}; };
export type GetCredential3rdQueryVariables = Exact<{ export type GetCredential3rdQueryVariables = Exact<{
filters: Credential3rdFilterInput; filter: Credential3rdFilterInput;
orderBy?: InputMaybe<Credential3rdOrderInput>; orderBy?: InputMaybe<Credential3rdOrderInput>;
pagination?: InputMaybe<PaginationInput>; pagination?: InputMaybe<PaginationInput>;
}>; }>;
@@ -1894,14 +2107,14 @@ export type InsertCredential3rdMutation = { __typename?: 'Mutation', credential3
export type UpdateCredential3rdMutationVariables = Exact<{ export type UpdateCredential3rdMutationVariables = Exact<{
data: Credential3rdUpdateInput; data: Credential3rdUpdateInput;
filters: Credential3rdFilterInput; filter: Credential3rdFilterInput;
}>; }>;
export type UpdateCredential3rdMutation = { __typename?: 'Mutation', credential3rdUpdate: Array<{ __typename?: 'Credential3rdBasic', id: number, cookies?: string | null, username?: string | null, password?: string | null, userAgent?: string | null, createdAt: string, updatedAt: string, credentialType: Credential3rdTypeEnum }> }; export type UpdateCredential3rdMutation = { __typename?: 'Mutation', credential3rdUpdate: Array<{ __typename?: 'Credential3rdBasic', id: number, cookies?: string | null, username?: string | null, password?: string | null, userAgent?: string | null, createdAt: string, updatedAt: string, credentialType: Credential3rdTypeEnum }> };
export type DeleteCredential3rdMutationVariables = Exact<{ export type DeleteCredential3rdMutationVariables = Exact<{
filters: Credential3rdFilterInput; filter: Credential3rdFilterInput;
}>; }>;
@@ -1914,12 +2127,12 @@ export type GetCredential3rdDetailQueryVariables = Exact<{
export type GetCredential3rdDetailQuery = { __typename?: 'Query', credential3rd: { __typename?: 'Credential3rdConnection', nodes: Array<{ __typename?: 'Credential3rd', id: number, cookies?: string | null, username?: string | null, password?: string | null, userAgent?: string | null, createdAt: string, updatedAt: string, credentialType: Credential3rdTypeEnum }> } }; export type GetCredential3rdDetailQuery = { __typename?: 'Query', credential3rd: { __typename?: 'Credential3rdConnection', nodes: Array<{ __typename?: 'Credential3rd', id: number, cookies?: string | null, username?: string | null, password?: string | null, userAgent?: string | null, createdAt: string, updatedAt: string, credentialType: Credential3rdTypeEnum }> } };
export type CheckCredential3rdAvailableQueryVariables = Exact<{ export type CheckCredential3rdAvailableMutationVariables = Exact<{
id: Scalars['Int']['input']; filter: Credential3rdFilterInput;
}>; }>;
export type CheckCredential3rdAvailableQuery = { __typename?: 'Query', credential3rdCheckAvailable: { __typename?: 'Credential3rdCheckAvailableInfo', available: boolean } }; export type CheckCredential3rdAvailableMutation = { __typename?: 'Mutation', credential3rdCheckAvailable: { __typename?: 'Credential3rdCheckAvailableInfo', available: boolean } };
export type InsertFeedMutationVariables = Exact<{ export type InsertFeedMutationVariables = Exact<{
data: FeedsInsertInput; data: FeedsInsertInput;
@@ -1929,14 +2142,14 @@ export type InsertFeedMutationVariables = Exact<{
export type InsertFeedMutation = { __typename?: 'Mutation', feedsCreateOne: { __typename?: 'FeedsBasic', id: number, createdAt: string, updatedAt: string, feedType: FeedTypeEnum, token: string } }; export type InsertFeedMutation = { __typename?: 'Mutation', feedsCreateOne: { __typename?: 'FeedsBasic', id: number, createdAt: string, updatedAt: string, feedType: FeedTypeEnum, token: string } };
export type DeleteFeedMutationVariables = Exact<{ export type DeleteFeedMutationVariables = Exact<{
filters: FeedsFilterInput; filter: FeedsFilterInput;
}>; }>;
export type DeleteFeedMutation = { __typename?: 'Mutation', feedsDelete: number }; export type DeleteFeedMutation = { __typename?: 'Mutation', feedsDelete: number };
export type GetSubscriptionsQueryVariables = Exact<{ export type GetSubscriptionsQueryVariables = Exact<{
filters: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
orderBy: SubscriptionsOrderInput; orderBy: SubscriptionsOrderInput;
pagination: PaginationInput; pagination: PaginationInput;
}>; }>;
@@ -1953,14 +2166,14 @@ export type InsertSubscriptionMutation = { __typename?: 'Mutation', subscription
export type UpdateSubscriptionsMutationVariables = Exact<{ export type UpdateSubscriptionsMutationVariables = Exact<{
data: SubscriptionsUpdateInput; data: SubscriptionsUpdateInput;
filters: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
}>; }>;
export type UpdateSubscriptionsMutation = { __typename?: 'Mutation', subscriptionsUpdate: Array<{ __typename?: 'SubscriptionsBasic', id: number, createdAt: string, updatedAt: string, displayName: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean }> }; export type UpdateSubscriptionsMutation = { __typename?: 'Mutation', subscriptionsUpdate: Array<{ __typename?: 'SubscriptionsBasic', id: number, createdAt: string, updatedAt: string, displayName: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean }> };
export type DeleteSubscriptionsMutationVariables = Exact<{ export type DeleteSubscriptionsMutationVariables = Exact<{
filters?: InputMaybe<SubscriptionsFilterInput>; filter?: InputMaybe<SubscriptionsFilterInput>;
}>; }>;
@@ -1971,31 +2184,31 @@ 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 }> }, 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, 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<{ export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
filter: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
}>; }>;
export type SyncSubscriptionFeedsIncrementalMutation = { __typename?: 'Mutation', subscriptionsSyncOneFeedsIncremental: { __typename?: 'SubscriberTasks', id: string } }; export type SyncSubscriptionFeedsIncrementalMutation = { __typename?: 'Mutation', subscriptionsSyncOneFeedsIncremental: { __typename?: 'SubscriberTasksBasic', id: string } };
export type SyncSubscriptionFeedsFullMutationVariables = Exact<{ export type SyncSubscriptionFeedsFullMutationVariables = Exact<{
filter: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
}>; }>;
export type SyncSubscriptionFeedsFullMutation = { __typename?: 'Mutation', subscriptionsSyncOneFeedsFull: { __typename?: 'SubscriberTasks', id: string } }; export type SyncSubscriptionFeedsFullMutation = { __typename?: 'Mutation', subscriptionsSyncOneFeedsFull: { __typename?: 'SubscriberTasksBasic', id: string } };
export type SyncSubscriptionSourcesMutationVariables = Exact<{ export type SyncSubscriptionSourcesMutationVariables = Exact<{
filter: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
}>; }>;
export type SyncSubscriptionSourcesMutation = { __typename?: 'Mutation', subscriptionsSyncOneSources: { __typename?: 'SubscriberTasks', id: string } }; export type SyncSubscriptionSourcesMutation = { __typename?: 'Mutation', subscriptionsSyncOneSources: { __typename?: 'SubscriberTasksBasic', id: string } };
export type GetTasksQueryVariables = Exact<{ export type GetTasksQueryVariables = Exact<{
filters: SubscriberTasksFilterInput; filter: SubscriberTasksFilterInput;
orderBy: SubscriberTasksOrderInput; orderBy: SubscriberTasksOrderInput;
pagination: PaginationInput; pagination: PaginationInput;
}>; }>;
@@ -2004,36 +2217,36 @@ export type GetTasksQueryVariables = Exact<{
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } }; export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
export type DeleteTasksMutationVariables = Exact<{ export type DeleteTasksMutationVariables = Exact<{
filters: SubscriberTasksFilterInput; filter: SubscriberTasksFilterInput;
}>; }>;
export type DeleteTasksMutation = { __typename?: 'Mutation', subscriberTasksDelete: number }; export type DeleteTasksMutation = { __typename?: 'Mutation', subscriberTasksDelete: number };
export type RetryTasksMutationVariables = Exact<{ export type RetryTasksMutationVariables = Exact<{
filters: SubscriberTasksFilterInput; filter: SubscriberTasksFilterInput;
}>; }>;
export type RetryTasksMutation = { __typename?: 'Mutation', subscriberTasksRetryOne: { __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number } }; export type RetryTasksMutation = { __typename?: 'Mutation', subscriberTasksRetryOne: { __typename?: 'SubscriberTasksBasic', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number } };
export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"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<GetCredential3rdQuery, GetCredential3rdQueryVariables>; export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"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"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"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<GetCredential3rdQuery, GetCredential3rdQueryVariables>;
export const InsertCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCreateOne"},"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode<InsertCredential3rdMutation, InsertCredential3rdMutationVariables>; export const InsertCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCreateOne"},"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode<InsertCredential3rdMutation, InsertCredential3rdMutationVariables>;
export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdUpdate"},"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":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode<UpdateCredential3rdMutation, UpdateCredential3rdMutationVariables>; export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdUpdate"},"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode<UpdateCredential3rdMutation, UpdateCredential3rdMutationVariables>;
export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteCredential3rdMutation, DeleteCredential3rdMutationVariables>; export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteCredential3rdMutation, DeleteCredential3rdMutationVariables>;
export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"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":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]}}]} as unknown as DocumentNode<GetCredential3rdDetailQuery, GetCredential3rdDetailQueryVariables>; export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"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":"credential3rd"},"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":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]}}]} as unknown as DocumentNode<GetCredential3rdDetailQuery, GetCredential3rdDetailQueryVariables>;
export const CheckCredential3rdAvailableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CheckCredential3rdAvailable"},"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":"credential3rdCheckAvailable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"available"}}]}}]}}]} as unknown as DocumentNode<CheckCredential3rdAvailableQuery, CheckCredential3rdAvailableQueryVariables>; export const CheckCredential3rdAvailableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCredential3rdAvailable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCheckAvailable"},"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":"available"}}]}}]}}]} as unknown as DocumentNode<CheckCredential3rdAvailableMutation, CheckCredential3rdAvailableMutationVariables>;
export const InsertFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsCreateOne"},"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":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]} as unknown as DocumentNode<InsertFeedMutation, InsertFeedMutationVariables>; export const InsertFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsCreateOne"},"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":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]} as unknown as DocumentNode<InsertFeedMutation, InsertFeedMutationVariables>;
export const DeleteFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteFeedMutation, DeleteFeedMutationVariables>; export const DeleteFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteFeedMutation, DeleteFeedMutationVariables>;
export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"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":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"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":"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"}}]}},{"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<GetSubscriptionsQuery, GetSubscriptionsQueryVariables>; export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"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":"subscriptions"},"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":"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"}}]}},{"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<GetSubscriptionsQuery, GetSubscriptionsQueryVariables>;
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<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>; 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<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
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":"filters"}},"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":"filters"}}}],"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<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>; 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<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
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":"filters"}},"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":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>; 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<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
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":"filters"},"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":"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<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>; 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<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
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<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>; 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<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
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<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>; 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<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
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<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>; 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<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
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":"filters"}},"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":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"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<GetTasksQuery, GetTasksQueryVariables>; 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<GetTasksQuery, GetTasksQueryVariables>;
export const DeleteTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteTasksMutation, DeleteTasksMutationVariables>; export const DeleteTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteTasksMutation, DeleteTasksMutationVariables>;
export const RetryTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksRetryOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"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"}}]}}]}}]} as unknown as DocumentNode<RetryTasksMutation, RetryTasksMutationVariables>; export const RetryTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksRetryOne"},"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"}},{"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"}}]}}]}}]} as unknown as DocumentNode<RetryTasksMutation, RetryTasksMutationVariables>;

View File

@@ -1,14 +1,6 @@
import type { ProLinkProps } from '@/components/ui/pro-link'; import type { ProLinkProps } from '@/components/ui/pro-link';
import { type } from 'arktype'; import { type } from 'arktype';
import { import type { LucideIcon } from 'lucide-react';
BookOpen,
Folders,
KeyRound,
type LucideIcon,
Settings2,
SquareTerminal,
Telescope,
} from 'lucide-react';
export interface NavMainItem { export interface NavMainItem {
link?: ProLinkProps; link?: ProLinkProps;

View File

@@ -6,14 +6,14 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { CHECK_CREDENTIAL_3RD_AVAILABLE } from '@/domains/recorder/schema/credential3rd'; import { CHECK_CREDENTIAL_3RD_AVAILABLE } from '@/domains/recorder/schema/credential3rd';
import { import { apolloErrorToMessage } from '@/infra/errors/apollo';
apolloErrorToMessage, import type {
getApolloQueryError, CheckCredential3rdAvailableMutation,
} from '@/infra/errors/apollo'; CheckCredential3rdAvailableMutationVariables,
import type { CheckCredential3rdAvailableQuery } from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import { useLazyQuery } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { CheckIcon, Loader2, XIcon } from 'lucide-react'; import { CheckIcon, Loader2, XIcon } from 'lucide-react';
import { memo, useCallback } from 'react'; import { memo } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
export interface Credential3rdCheckAvailableViewProps { export interface Credential3rdCheckAvailableViewProps {
@@ -22,30 +22,23 @@ export interface Credential3rdCheckAvailableViewProps {
export const Credential3rdCheckAvailableView = memo( export const Credential3rdCheckAvailableView = memo(
({ id }: Credential3rdCheckAvailableViewProps) => { ({ id }: Credential3rdCheckAvailableViewProps) => {
const [checkAvailable, { data, error, loading }] = const [checkAvailable, { data, error, loading }] = useMutation<
useLazyQuery<CheckCredential3rdAvailableQuery>( CheckCredential3rdAvailableMutation,
CHECK_CREDENTIAL_3RD_AVAILABLE, CheckCredential3rdAvailableMutationVariables
{ >(CHECK_CREDENTIAL_3RD_AVAILABLE, {
variables: { id }, onCompleted: (data) => {
} if (data.credential3rdCheckAvailable.available) {
);
const handleCheckAvailable = useCallback(async () => {
const checkResult = await checkAvailable();
const error = getApolloQueryError(checkResult);
console.error('error', error);
if (error) {
toast.error('Failed to check available', {
description: apolloErrorToMessage(error),
});
return;
}
if (checkResult.data?.credential3rdCheckAvailable.available) {
toast.success('Credential is available'); toast.success('Credential is available');
} else { } else {
toast.error('Credential is not available'); toast.error('Credential is not available');
} }
}, [checkAvailable]); },
onError: (error) => {
toast.error('Failed to check available', {
description: apolloErrorToMessage(error),
});
},
});
const available = data?.credential3rdCheckAvailable?.available; const available = data?.credential3rdCheckAvailable?.available;
@@ -54,7 +47,11 @@ export const Credential3rdCheckAvailableView = memo(
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
onClick={handleCheckAvailable} onClick={() =>
checkAvailable({
variables: { filter: { id: { eq: id } } },
})
}
disabled={loading} disabled={loading}
> >
<span> Check Available </span> <span> Check Available </span>

Some files were not shown because too many files have changed in this diff Show More