refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate

- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦
- 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块
- 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支
- 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合
- 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor
- 前端适配:调整 admin API 调用和 provider 模型测试对话框
This commit is contained in:
fawney19
2026-04-07 02:50:19 +08:00
parent 763ff03a7b
commit 5d96d6673b
732 changed files with 28593 additions and 20666 deletions

View File

@@ -3,7 +3,7 @@ use std::sync::RwLock;
use async_trait::async_trait;
use super::types::{
use super::{
StoredVideoTask, UpsertVideoTask, VideoTaskLookupKey, VideoTaskModelCount,
VideoTaskQueryFilter, VideoTaskReadRepository, VideoTaskStatus, VideoTaskStatusCount,
VideoTaskWriteRepository,
@@ -140,9 +140,9 @@ impl VideoTaskReadRepository for InMemoryVideoTaskRepository {
.filter(|task| {
matches!(
task.status,
super::types::VideoTaskStatus::Submitted
| super::types::VideoTaskStatus::Queued
| super::types::VideoTaskStatus::Processing
super::VideoTaskStatus::Submitted
| super::VideoTaskStatus::Queued
| super::VideoTaskStatus::Processing
) && task.poll_count < task.max_poll_count
&& task
.next_poll_at_unix_secs

View File

@@ -1,11 +1,11 @@
mod memory;
mod sql;
mod types;
pub use memory::InMemoryVideoTaskRepository;
pub use sql::{SqlxVideoTaskReadRepository, SqlxVideoTaskRepository};
pub use types::{
#[allow(unused_imports)]
pub(crate) use aether_data_contracts::repository::video_tasks::{
StoredVideoTask, UpsertVideoTask, VideoTaskLookupKey, VideoTaskModelCount,
VideoTaskQueryFilter, VideoTaskReadRepository, VideoTaskRepository, VideoTaskStatus,
VideoTaskStatusCount, VideoTaskWriteRepository,
};
pub use memory::InMemoryVideoTaskRepository;
pub use sql::{SqlxVideoTaskReadRepository, SqlxVideoTaskRepository};

View File

@@ -2,6 +2,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Row};
use async_trait::async_trait;
use crate::error::SqlxResultExt;
use crate::repository::video_tasks::{
StoredVideoTask, UpsertVideoTask, VideoTaskLookupKey, VideoTaskModelCount,
VideoTaskQueryFilter, VideoTaskReadRepository, VideoTaskStatus, VideoTaskStatusCount,
@@ -302,7 +303,8 @@ impl SqlxVideoTaskRepository {
let row = sqlx::query(&sql)
.bind(id)
.fetch_optional(&self.pool)
.await?;
.await
.map_postgres_err()?;
row.as_ref().map(map_video_task_row).transpose()
}
@@ -314,7 +316,8 @@ impl SqlxVideoTaskRepository {
let row = sqlx::query(&sql)
.bind(short_id)
.fetch_optional(&self.pool)
.await?;
.await
.map_postgres_err()?;
row.as_ref().map(map_video_task_row).transpose()
}
@@ -328,7 +331,8 @@ impl SqlxVideoTaskRepository {
.bind(user_id)
.bind(external_task_id)
.fetch_optional(&self.pool)
.await?;
.await
.map_postgres_err()?;
row.as_ref().map(map_video_task_row).transpose()
}
@@ -345,7 +349,8 @@ impl SqlxVideoTaskRepository {
DataLayerError::UnexpectedValue(format!("invalid active task limit: {limit}"))
})?)
.fetch_all(&self.pool)
.await?;
.await
.map_postgres_err()?;
rows.iter().map(map_video_task_row).collect()
}
@@ -368,7 +373,8 @@ impl SqlxVideoTaskRepository {
DataLayerError::UnexpectedValue(format!("invalid due task limit: {limit}"))
})?)
.fetch_all(&self.pool)
.await?;
.await
.map_postgres_err()?;
rows.iter().map(map_video_task_row).collect()
}
@@ -398,7 +404,11 @@ impl SqlxVideoTaskRepository {
builder.push("\nLIMIT ");
builder.push_bind(limit);
let rows = builder.build().fetch_all(&self.pool).await?;
let rows = builder
.build()
.fetch_all(&self.pool)
.await
.map_postgres_err()?;
rows.iter().map(map_video_task_row).collect()
}
@@ -407,8 +417,12 @@ impl SqlxVideoTaskRepository {
QueryBuilder::<Postgres>::new("SELECT COUNT(id) AS total FROM video_tasks");
push_video_task_filter(&mut builder, filter, None);
let row = builder.build().fetch_one(&self.pool).await?;
let total = row.try_get::<i64, _>("total")?;
let row = builder
.build()
.fetch_one(&self.pool)
.await
.map_postgres_err()?;
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
u64::try_from(total)
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid count result: {total}")))
}
@@ -422,12 +436,19 @@ impl SqlxVideoTaskRepository {
push_video_task_filter(&mut builder, filter, None);
builder.push("\nGROUP BY status\nORDER BY status ASC");
let rows = builder.build().fetch_all(&self.pool).await?;
let rows = builder
.build()
.fetch_all(&self.pool)
.await
.map_postgres_err()?;
rows.into_iter()
.map(|row| {
let status =
VideoTaskStatus::from_database(row.try_get::<String, _>("status")?.as_str())?;
let total = row.try_get::<i64, _>("total")?;
let status = VideoTaskStatus::from_database(
row.try_get::<String, _>("status")
.map_postgres_err()?
.as_str(),
)?;
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
Ok(VideoTaskStatusCount {
status,
count: u64::try_from(total).map_err(|_| {
@@ -452,8 +473,12 @@ impl SqlxVideoTaskRepository {
push_sql_clause(&mut builder, &mut has_where, "user_id IS NOT NULL");
push_sql_clause(&mut builder, &mut has_where, "user_id <> ''");
let row = builder.build().fetch_one(&self.pool).await?;
let total = row.try_get::<i64, _>("total")?;
let row = builder
.build()
.fetch_one(&self.pool)
.await
.map_postgres_err()?;
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
u64::try_from(total)
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid count result: {total}")))
}
@@ -479,11 +504,15 @@ impl SqlxVideoTaskRepository {
builder.push("\nGROUP BY model\nORDER BY total DESC, model ASC\nLIMIT ");
builder.push_bind(limit);
let rows = builder.build().fetch_all(&self.pool).await?;
let rows = builder
.build()
.fetch_all(&self.pool)
.await
.map_postgres_err()?;
rows.into_iter()
.map(|row| {
let model = row.try_get::<String, _>("model")?;
let total = row.try_get::<i64, _>("total")?;
let model = row.try_get::<String, _>("model").map_postgres_err()?;
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
Ok(VideoTaskModelCount {
model,
count: u64::try_from(total).map_err(|_| {
@@ -505,8 +534,12 @@ impl SqlxVideoTaskRepository {
QueryBuilder::<Postgres>::new("SELECT COUNT(id) AS total FROM video_tasks");
push_video_task_filter(&mut builder, filter, Some(created_since_unix_secs));
let row = builder.build().fetch_one(&self.pool).await?;
let total = row.try_get::<i64, _>("total")?;
let row = builder
.build()
.fetch_one(&self.pool)
.await
.map_postgres_err()?;
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
u64::try_from(total)
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid count result: {total}")))
}
@@ -571,7 +604,8 @@ impl SqlxVideoTaskRepository {
.bind(task.completed_at_unix_secs.map(|value| value as f64))
.bind(task.updated_at_unix_secs as f64)
.fetch_one(&self.pool)
.await?;
.await
.map_postgres_err()?;
map_video_task_row(&row)
}
@@ -640,7 +674,8 @@ impl SqlxVideoTaskRepository {
.bind(task.updated_at_unix_secs as f64)
.bind(vec!["pending", "submitted", "queued", "processing"])
.fetch_optional(&self.pool)
.await?;
.await
.map_postgres_err()?;
row.as_ref().map(map_video_task_row).transpose()
}
@@ -665,7 +700,8 @@ impl SqlxVideoTaskRepository {
.bind(claim_until_unix_secs as f64)
.bind(now_unix_secs as f64)
.fetch_all(&self.pool)
.await?;
.await
.map_postgres_err()?;
let mut tasks = rows
.iter()
@@ -846,45 +882,49 @@ fn map_status_for_database(status: VideoTaskStatus) -> &'static str {
}
fn map_video_task_row(row: &sqlx::postgres::PgRow) -> Result<StoredVideoTask, DataLayerError> {
let status = VideoTaskStatus::from_database(row.try_get::<String, _>("status")?.as_str())?;
let status = VideoTaskStatus::from_database(
row.try_get::<String, _>("status")
.map_postgres_err()?
.as_str(),
)?;
StoredVideoTask::new(
row.try_get("id")?,
row.try_get("short_id")?,
row.try_get("request_id")?,
row.try_get("user_id")?,
row.try_get("api_key_id")?,
row.try_get("username")?,
row.try_get("api_key_name")?,
row.try_get("external_task_id")?,
row.try_get("provider_id")?,
row.try_get("endpoint_id")?,
row.try_get("key_id")?,
row.try_get("client_api_format")?,
row.try_get("provider_api_format")?,
row.try_get("format_converted")?,
row.try_get("model")?,
row.try_get("prompt")?,
row.try_get("original_request_body")?,
row.try_get("duration_seconds")?,
row.try_get("resolution")?,
row.try_get("aspect_ratio")?,
row.try_get("size")?,
row.try_get("id").map_postgres_err()?,
row.try_get("short_id").map_postgres_err()?,
row.try_get("request_id").map_postgres_err()?,
row.try_get("user_id").map_postgres_err()?,
row.try_get("api_key_id").map_postgres_err()?,
row.try_get("username").map_postgres_err()?,
row.try_get("api_key_name").map_postgres_err()?,
row.try_get("external_task_id").map_postgres_err()?,
row.try_get("provider_id").map_postgres_err()?,
row.try_get("endpoint_id").map_postgres_err()?,
row.try_get("key_id").map_postgres_err()?,
row.try_get("client_api_format").map_postgres_err()?,
row.try_get("provider_api_format").map_postgres_err()?,
row.try_get("format_converted").map_postgres_err()?,
row.try_get("model").map_postgres_err()?,
row.try_get("prompt").map_postgres_err()?,
row.try_get("original_request_body").map_postgres_err()?,
row.try_get("duration_seconds").map_postgres_err()?,
row.try_get("resolution").map_postgres_err()?,
row.try_get("aspect_ratio").map_postgres_err()?,
row.try_get("size").map_postgres_err()?,
status,
row.try_get("progress_percent")?,
row.try_get("progress_message")?,
row.try_get("retry_count")?,
row.try_get("poll_interval_seconds")?,
row.try_get("next_poll_at_unix_secs")?,
row.try_get("poll_count")?,
row.try_get("max_poll_count")?,
row.try_get("created_at_unix_secs")?,
row.try_get("submitted_at_unix_secs")?,
row.try_get("completed_at_unix_secs")?,
row.try_get("updated_at_unix_secs")?,
row.try_get("error_code")?,
row.try_get("error_message")?,
row.try_get("video_url")?,
row.try_get("request_metadata")?,
row.try_get("progress_percent").map_postgres_err()?,
row.try_get("progress_message").map_postgres_err()?,
row.try_get("retry_count").map_postgres_err()?,
row.try_get("poll_interval_seconds").map_postgres_err()?,
row.try_get("next_poll_at_unix_secs").map_postgres_err()?,
row.try_get("poll_count").map_postgres_err()?,
row.try_get("max_poll_count").map_postgres_err()?,
row.try_get("created_at_unix_secs").map_postgres_err()?,
row.try_get("submitted_at_unix_secs").map_postgres_err()?,
row.try_get("completed_at_unix_secs").map_postgres_err()?,
row.try_get("updated_at_unix_secs").map_postgres_err()?,
row.try_get("error_code").map_postgres_err()?,
row.try_get("error_message").map_postgres_err()?,
row.try_get("video_url").map_postgres_err()?,
row.try_get("request_metadata").map_postgres_err()?,
)
}

View File

@@ -1,611 +0,0 @@
use async_trait::async_trait;
use serde_json::Value;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum VideoTaskStatus {
Pending,
Submitted,
Queued,
Processing,
Completed,
Failed,
Cancelled,
Expired,
Deleted,
}
impl VideoTaskStatus {
pub fn from_database(value: &str) -> Result<Self, crate::DataLayerError> {
match value.trim().to_ascii_lowercase().as_str() {
"pending" => Ok(Self::Pending),
"submitted" => Ok(Self::Submitted),
"queued" => Ok(Self::Queued),
"processing" => Ok(Self::Processing),
"completed" => Ok(Self::Completed),
"failed" => Ok(Self::Failed),
"cancelled" => Ok(Self::Cancelled),
"expired" => Ok(Self::Expired),
"deleted" => Ok(Self::Deleted),
other => Err(crate::DataLayerError::UnexpectedValue(format!(
"unsupported video_tasks.status: {other}"
))),
}
}
pub fn is_active(self) -> bool {
matches!(
self,
Self::Pending | Self::Submitted | Self::Queued | Self::Processing
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StoredVideoTask {
pub id: String,
pub short_id: Option<String>,
pub request_id: String,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
pub username: Option<String>,
pub api_key_name: Option<String>,
pub external_task_id: Option<String>,
pub provider_id: Option<String>,
pub endpoint_id: Option<String>,
pub key_id: Option<String>,
pub client_api_format: Option<String>,
pub provider_api_format: Option<String>,
pub format_converted: bool,
pub model: Option<String>,
pub prompt: Option<String>,
pub original_request_body: Option<Value>,
pub duration_seconds: Option<u32>,
pub resolution: Option<String>,
pub aspect_ratio: Option<String>,
pub size: Option<String>,
pub status: VideoTaskStatus,
pub progress_percent: u16,
pub progress_message: Option<String>,
pub retry_count: u32,
pub poll_interval_seconds: u32,
pub next_poll_at_unix_secs: Option<u64>,
pub poll_count: u32,
pub max_poll_count: u32,
pub created_at_unix_secs: u64,
pub submitted_at_unix_secs: Option<u64>,
pub completed_at_unix_secs: Option<u64>,
pub updated_at_unix_secs: u64,
pub error_code: Option<String>,
pub error_message: Option<String>,
pub video_url: Option<String>,
pub request_metadata: Option<Value>,
}
impl StoredVideoTask {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
short_id: Option<String>,
request_id: String,
user_id: Option<String>,
api_key_id: Option<String>,
username: Option<String>,
api_key_name: Option<String>,
external_task_id: Option<String>,
provider_id: Option<String>,
endpoint_id: Option<String>,
key_id: Option<String>,
client_api_format: Option<String>,
provider_api_format: Option<String>,
format_converted: bool,
model: Option<String>,
prompt: Option<String>,
original_request_body: Option<Value>,
duration_seconds: Option<i32>,
resolution: Option<String>,
aspect_ratio: Option<String>,
size: Option<String>,
status: VideoTaskStatus,
progress_percent: i32,
progress_message: Option<String>,
retry_count: i32,
poll_interval_seconds: i32,
next_poll_at_unix_secs: Option<i64>,
poll_count: i32,
max_poll_count: i32,
created_at_unix_secs: i64,
submitted_at_unix_secs: Option<i64>,
completed_at_unix_secs: Option<i64>,
updated_at_unix_secs: i64,
error_code: Option<String>,
error_message: Option<String>,
video_url: Option<String>,
request_metadata: Option<Value>,
) -> Result<Self, crate::DataLayerError> {
let progress_percent = u16::try_from(progress_percent).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!(
"invalid progress_percent: {progress_percent}"
))
})?;
let retry_count = u32::try_from(retry_count).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!("invalid retry_count: {retry_count}"))
})?;
let poll_interval_seconds = u32::try_from(poll_interval_seconds).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!(
"invalid poll_interval_seconds: {poll_interval_seconds}"
))
})?;
let next_poll_at_unix_secs =
coerce_optional_unix_secs(next_poll_at_unix_secs, "next_poll_at_unix_secs")?;
let poll_count = u32::try_from(poll_count).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!("invalid poll_count: {poll_count}"))
})?;
let max_poll_count = u32::try_from(max_poll_count).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!(
"invalid max_poll_count: {max_poll_count}"
))
})?;
let created_at_unix_secs = u64::try_from(created_at_unix_secs).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!(
"invalid created_at_unix_secs: {created_at_unix_secs}"
))
})?;
let submitted_at_unix_secs =
coerce_optional_unix_secs(submitted_at_unix_secs, "submitted_at_unix_secs")?;
let completed_at_unix_secs =
coerce_optional_unix_secs(completed_at_unix_secs, "completed_at_unix_secs")?;
let updated_at_unix_secs = u64::try_from(updated_at_unix_secs).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!(
"invalid updated_at_unix_secs: {updated_at_unix_secs}"
))
})?;
let duration_seconds = match duration_seconds {
Some(value) => Some(u32::try_from(value).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!("invalid duration_seconds: {value}"))
})?),
None => None,
};
Ok(Self {
id,
short_id,
request_id,
user_id,
api_key_id,
username,
api_key_name,
external_task_id,
provider_id,
endpoint_id,
key_id,
client_api_format,
provider_api_format,
format_converted,
model,
prompt,
original_request_body,
duration_seconds,
resolution,
aspect_ratio,
size,
status,
progress_percent,
progress_message,
retry_count,
poll_interval_seconds,
next_poll_at_unix_secs,
poll_count,
max_poll_count,
created_at_unix_secs,
submitted_at_unix_secs,
completed_at_unix_secs,
updated_at_unix_secs,
error_code,
error_message,
video_url,
request_metadata,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpsertVideoTask {
pub id: String,
pub short_id: Option<String>,
pub request_id: String,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
pub username: Option<String>,
pub api_key_name: Option<String>,
pub external_task_id: Option<String>,
pub provider_id: Option<String>,
pub endpoint_id: Option<String>,
pub key_id: Option<String>,
pub client_api_format: Option<String>,
pub provider_api_format: Option<String>,
pub format_converted: bool,
pub model: Option<String>,
pub prompt: Option<String>,
pub original_request_body: Option<Value>,
pub duration_seconds: Option<u32>,
pub resolution: Option<String>,
pub aspect_ratio: Option<String>,
pub size: Option<String>,
pub status: VideoTaskStatus,
pub progress_percent: u16,
pub progress_message: Option<String>,
pub retry_count: u32,
pub poll_interval_seconds: u32,
pub next_poll_at_unix_secs: Option<u64>,
pub poll_count: u32,
pub max_poll_count: u32,
pub created_at_unix_secs: u64,
pub submitted_at_unix_secs: Option<u64>,
pub completed_at_unix_secs: Option<u64>,
pub updated_at_unix_secs: u64,
pub error_code: Option<String>,
pub error_message: Option<String>,
pub video_url: Option<String>,
pub request_metadata: Option<Value>,
}
impl UpsertVideoTask {
pub fn into_stored(self) -> StoredVideoTask {
StoredVideoTask {
id: self.id,
short_id: self.short_id,
request_id: self.request_id,
user_id: self.user_id,
api_key_id: self.api_key_id,
username: self.username,
api_key_name: self.api_key_name,
external_task_id: self.external_task_id,
provider_id: self.provider_id,
endpoint_id: self.endpoint_id,
key_id: self.key_id,
client_api_format: self.client_api_format,
provider_api_format: self.provider_api_format,
format_converted: self.format_converted,
model: self.model,
prompt: self.prompt,
original_request_body: self.original_request_body,
duration_seconds: self.duration_seconds,
resolution: self.resolution,
aspect_ratio: self.aspect_ratio,
size: self.size,
status: self.status,
progress_percent: self.progress_percent,
progress_message: self.progress_message,
retry_count: self.retry_count,
poll_interval_seconds: self.poll_interval_seconds,
next_poll_at_unix_secs: self.next_poll_at_unix_secs,
poll_count: self.poll_count,
max_poll_count: self.max_poll_count,
created_at_unix_secs: self.created_at_unix_secs,
submitted_at_unix_secs: self.submitted_at_unix_secs,
completed_at_unix_secs: self.completed_at_unix_secs,
updated_at_unix_secs: self.updated_at_unix_secs,
error_code: self.error_code,
error_message: self.error_message,
video_url: self.video_url,
request_metadata: self.request_metadata,
}
}
}
impl From<StoredVideoTask> for UpsertVideoTask {
fn from(task: StoredVideoTask) -> Self {
Self {
id: task.id,
short_id: task.short_id,
request_id: task.request_id,
user_id: task.user_id,
api_key_id: task.api_key_id,
username: task.username,
api_key_name: task.api_key_name,
external_task_id: task.external_task_id,
provider_id: task.provider_id,
endpoint_id: task.endpoint_id,
key_id: task.key_id,
client_api_format: task.client_api_format,
provider_api_format: task.provider_api_format,
format_converted: task.format_converted,
model: task.model,
prompt: task.prompt,
original_request_body: task.original_request_body,
duration_seconds: task.duration_seconds,
resolution: task.resolution,
aspect_ratio: task.aspect_ratio,
size: task.size,
status: task.status,
progress_percent: task.progress_percent,
progress_message: task.progress_message,
retry_count: task.retry_count,
poll_interval_seconds: task.poll_interval_seconds,
next_poll_at_unix_secs: task.next_poll_at_unix_secs,
poll_count: task.poll_count,
max_poll_count: task.max_poll_count,
created_at_unix_secs: task.created_at_unix_secs,
submitted_at_unix_secs: task.submitted_at_unix_secs,
completed_at_unix_secs: task.completed_at_unix_secs,
updated_at_unix_secs: task.updated_at_unix_secs,
error_code: task.error_code,
error_message: task.error_message,
video_url: task.video_url,
request_metadata: task.request_metadata,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoTaskLookupKey<'a> {
Id(&'a str),
ShortId(&'a str),
UserExternal {
user_id: &'a str,
external_task_id: &'a str,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VideoTaskQueryFilter {
pub user_id: Option<String>,
pub status: Option<VideoTaskStatus>,
pub model_substring: Option<String>,
pub client_api_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct VideoTaskStatusCount {
pub status: VideoTaskStatus,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct VideoTaskModelCount {
pub model: String,
pub count: u64,
}
#[async_trait]
pub trait VideoTaskReadRepository: Send + Sync {
async fn find(
&self,
key: VideoTaskLookupKey<'_>,
) -> Result<Option<StoredVideoTask>, crate::DataLayerError>;
async fn list_active(
&self,
limit: usize,
) -> Result<Vec<StoredVideoTask>, crate::DataLayerError>;
async fn list_due(
&self,
now_unix_secs: u64,
limit: usize,
) -> Result<Vec<StoredVideoTask>, crate::DataLayerError>;
async fn list_page(
&self,
filter: &VideoTaskQueryFilter,
offset: usize,
limit: usize,
) -> Result<Vec<StoredVideoTask>, crate::DataLayerError>;
async fn count(&self, filter: &VideoTaskQueryFilter) -> Result<u64, crate::DataLayerError>;
async fn count_by_status(
&self,
filter: &VideoTaskQueryFilter,
) -> Result<Vec<VideoTaskStatusCount>, crate::DataLayerError>;
async fn count_distinct_users(
&self,
filter: &VideoTaskQueryFilter,
) -> Result<u64, crate::DataLayerError>;
async fn top_models(
&self,
filter: &VideoTaskQueryFilter,
limit: usize,
) -> Result<Vec<VideoTaskModelCount>, crate::DataLayerError>;
async fn count_created_since(
&self,
filter: &VideoTaskQueryFilter,
created_since_unix_secs: u64,
) -> Result<u64, crate::DataLayerError>;
}
#[async_trait]
pub trait VideoTaskWriteRepository: Send + Sync {
async fn upsert(&self, task: UpsertVideoTask)
-> Result<StoredVideoTask, crate::DataLayerError>;
async fn update_if_active(
&self,
task: UpsertVideoTask,
) -> Result<Option<StoredVideoTask>, crate::DataLayerError>;
async fn claim_due(
&self,
now_unix_secs: u64,
claim_until_unix_secs: u64,
limit: usize,
) -> Result<Vec<StoredVideoTask>, crate::DataLayerError>;
}
pub trait VideoTaskRepository:
VideoTaskReadRepository + VideoTaskWriteRepository + Send + Sync
{
}
impl<T> VideoTaskRepository for T where
T: VideoTaskReadRepository + VideoTaskWriteRepository + Send + Sync
{
}
fn coerce_optional_unix_secs(
value: Option<i64>,
field: &str,
) -> Result<Option<u64>, crate::DataLayerError> {
match value {
Some(value) => Ok(Some(u64::try_from(value).map_err(|_| {
crate::DataLayerError::UnexpectedValue(format!("invalid {field}: {value}"))
})?)),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::{StoredVideoTask, VideoTaskStatus};
#[allow(clippy::type_complexity)]
fn base_new_args() -> (
String,
Option<String>,
String,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
bool,
Option<String>,
Option<String>,
Option<serde_json::Value>,
Option<i32>,
Option<String>,
Option<String>,
Option<String>,
VideoTaskStatus,
i32,
Option<String>,
i32,
i32,
Option<i64>,
i32,
i32,
i64,
Option<i64>,
Option<i64>,
i64,
Option<String>,
Option<String>,
Option<String>,
Option<serde_json::Value>,
) {
(
"task-1".to_string(),
None,
"request-1".to_string(),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
false,
None,
None,
None,
None,
None,
None,
None,
VideoTaskStatus::Submitted,
10,
None,
0,
10,
Some(1),
0,
360,
1,
None,
None,
1,
None,
None,
None,
None,
)
}
#[test]
fn parses_status_from_database_text() {
assert_eq!(
VideoTaskStatus::from_database("processing").expect("status should parse"),
VideoTaskStatus::Processing
);
}
#[test]
fn rejects_invalid_database_status() {
assert!(VideoTaskStatus::from_database("mystery").is_err());
}
#[test]
fn rejects_invalid_numeric_fields() {
let mut args = base_new_args();
args.22 = -1;
assert!(StoredVideoTask::new(
args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
args.10, args.11, args.12, args.13, args.14, args.15, args.16, args.17, args.18,
args.19, args.20, args.21, args.22, args.23, args.24, args.25, args.26, args.27,
args.28, args.29, args.30, args.31, args.32, args.33, args.34, args.35, args.36,
)
.is_err());
}
#[test]
fn rejects_negative_updated_at_values() {
let mut args = base_new_args();
args.32 = -1;
assert!(StoredVideoTask::new(
args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
args.10, args.11, args.12, args.13, args.14, args.15, args.16, args.17, args.18,
args.19, args.20, args.21, args.22, args.23, args.24, args.25, args.26, args.27,
args.28, args.29, args.30, args.31, args.32, args.33, args.34, args.35, args.36,
)
.is_err());
}
#[test]
fn rejects_negative_created_at_values() {
let mut args = base_new_args();
args.29 = -1;
assert!(StoredVideoTask::new(
args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
args.10, args.11, args.12, args.13, args.14, args.15, args.16, args.17, args.18,
args.19, args.20, args.21, args.22, args.23, args.24, args.25, args.26, args.27,
args.28, args.29, args.30, args.31, args.32, args.33, args.34, args.35, args.36,
)
.is_err());
}
#[test]
fn rejects_negative_optional_completed_at_values() {
let mut args = base_new_args();
args.31 = Some(-1);
assert!(StoredVideoTask::new(
args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
args.10, args.11, args.12, args.13, args.14, args.15, args.16, args.17, args.18,
args.19, args.20, args.21, args.22, args.23, args.24, args.25, args.26, args.27,
args.28, args.29, args.30, args.31, args.32, args.33, args.34, args.35, args.36,
)
.is_err());
}
}