From 819caae9147e545b194ae12f43e62c7ca84e65c0 Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Wed, 23 Apr 2025 12:57:10 -0700 Subject: [PATCH] implement watchdog timers --- Cargo.lock | 32 +++ Cargo.toml | 1 + .../2025-04-22-053211_watchdogs/down.sql | 1 + migrations/2025-04-22-053211_watchdogs/up.sql | 10 + src/api_keys.rs | 48 ++-- src/channel_selections.rs | 4 +- src/channels.rs | 23 +- src/channels_router.rs | 140 +++++++---- src/csrf.rs | 26 +- src/governors.rs | 208 +++++++++++----- src/main.rs | 1 + src/messages.rs | 77 +++++- src/projects.rs | 110 ++++++--- src/projects_router.rs | 87 ++++--- src/schema.rs | 13 + src/team_invitations.rs | 14 +- src/team_memberships.rs | 8 +- src/teams.rs | 26 +- src/teams_router.rs | 96 +++++--- src/users.rs | 10 +- src/v0_router.rs | 230 +++++++++--------- src/watchdogs.rs | 67 +++++ src/worker.rs | 78 +++++- templates/project.html | 84 +++++-- templates/projects.html | 4 +- 25 files changed, 964 insertions(+), 434 deletions(-) create mode 100644 migrations/2025-04-22-053211_watchdogs/down.sql create mode 100644 migrations/2025-04-22-053211_watchdogs/up.sql create mode 100644 src/watchdogs.rs diff --git a/Cargo.lock b/Cargo.lock index 7a1062b..3f004bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,37 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "diesel" version = "2.2.8" @@ -2644,6 +2675,7 @@ dependencies = [ "clap", "config", "deadpool-diesel", + "derive_builder", "diesel", "diesel_migrations", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 0d28dc9..57e4aed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ chrono = { version = "0.4.39", features = ["serde"] } clap = { version = "4.5.31", features = ["derive"] } config = "0.14.1" deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] } +derive_builder = "0.20.2" diesel = { version = "2.2.6", features = ["postgres", "chrono", "uuid", "serde_json"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } dotenvy = "0.15.7" diff --git a/migrations/2025-04-22-053211_watchdogs/down.sql b/migrations/2025-04-22-053211_watchdogs/down.sql new file mode 100644 index 0000000..2214cff --- /dev/null +++ b/migrations/2025-04-22-053211_watchdogs/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS watchdogs; diff --git a/migrations/2025-04-22-053211_watchdogs/up.sql b/migrations/2025-04-22-053211_watchdogs/up.sql new file mode 100644 index 0000000..ddaac01 --- /dev/null +++ b/migrations/2025-04-22-053211_watchdogs/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS watchdogs ( + id UUID PRIMARY KEY NOT NULL, + project_id UUID UNIQUE NOT NULL REFERENCES projects (id) ON DELETE RESTRICT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_set_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expiration TIMESTAMPTZ NOT NULL, + notified BOOLEAN NOT NULL DEFAULT FALSE +); +CREATE INDEX ON watchdogs (project_id); +CREATE INDEX ON watchdogs (expiration); diff --git a/src/api_keys.rs b/src/api_keys.rs index 5e6bfeb..185059d 100644 --- a/src/api_keys.rs +++ b/src/api_keys.rs @@ -1,21 +1,21 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{DateTime, Utc}; -use deadpool_diesel::postgres::Connection; use diesel::{ - dsl::{auto_type, AsSelect}, + dsl::{auto_type, update, AsSelect}, pg::Pg, prelude::*, }; use uuid::Uuid; -use crate::{app_error::AppError, schema::api_keys, teams::Team}; +use crate::{app_error::AppError, schema::api_keys}; + +pub use crate::schema::api_keys::{dsl, table}; /// A team-scoped application key for authenticating API calls to /say, etc. /// Does not authorize any administrative functions besides creating projects. -#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)] +#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)] #[diesel(table_name = api_keys)] -#[diesel(belongs_to(Team))] pub struct ApiKey { pub id: Uuid, pub team_id: Uuid, @@ -23,38 +23,28 @@ pub struct ApiKey { } impl ApiKey { - pub async fn generate_for_team(db_conn: &Connection, team_id: Uuid) -> Result { - let api_key = Self { + pub fn new_from_team_id(team_id: Uuid) -> Self { + Self { team_id, id: Uuid::new_v4(), last_used_at: None, - }; - let api_key_copy = api_key.clone(); - db_conn - .interact(move |conn| { - diesel::insert_into(api_keys::table) - .values(api_key_copy) - .execute(conn) - }) - .await - .unwrap()?; - Ok(api_key) + } } #[auto_type(no_type_alias)] pub fn all() -> _ { - let select: AsSelect = ApiKey::as_select(); - api_keys::table.select(select) + let select: AsSelect = Self::as_select(); + table.select(select) } #[auto_type(no_type_alias)] pub fn with_id<'a>(id: &'a Uuid) -> _ { - api_keys::id.eq(id) + dsl::id.eq(id) } #[auto_type(no_type_alias)] pub fn with_team<'a>(team_id: &'a Uuid) -> _ { - api_keys::team_id.eq(team_id) + dsl::team_id.eq(team_id) } } @@ -78,3 +68,15 @@ pub fn try_parse_as_uuid(value: &str) -> Result { Uuid::try_parse(value).or(Err(anyhow::anyhow!("failed to parse"))) } } + +pub fn use_api_key(key_id: &str, db_conn: &mut PgConnection) -> Result { + let normalized_id = + try_parse_as_uuid(key_id).or(Err(AppError::Forbidden("Key not accepted.".to_string())))?; + update(table.filter(ApiKey::with_id(&normalized_id))) + .set(dsl::last_used_at.eq(diesel::dsl::now)) + .returning(ApiKey::as_returning()) + .get_result(db_conn) + .optional() + .context("failed to load api key")? + .ok_or(AppError::Forbidden("Key not accepted.".to_owned())) +} diff --git a/src/channel_selections.rs b/src/channel_selections.rs index c88a1f6..35b8303 100644 --- a/src/channel_selections.rs +++ b/src/channel_selections.rs @@ -7,7 +7,9 @@ use uuid::Uuid; use crate::schema::channel_selections; -#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)] +pub use crate::schema::channel_selections::{dsl, table}; + +#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)] #[diesel(belongs_to(crate::channels::Channel))] #[diesel(belongs_to(crate::projects::Project))] #[diesel(primary_key(channel_id, project_id))] diff --git a/src/channels.rs b/src/channels.rs index 5bae8cc..7dcc31f 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,5 +1,6 @@ use std::fmt::Debug; +use derive_builder::Builder; use diesel::{ backend::Backend, deserialize::{self, FromSql, FromSqlRow}, @@ -14,7 +15,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use uuid::Uuid; -use crate::{schema::channels, teams::Team}; +use crate::schema::channels; pub const CHANNEL_BACKEND_EMAIL: &str = "email"; pub const CHANNEL_BACKEND_SLACK: &str = "slack"; @@ -25,8 +26,7 @@ pub use crate::schema::channels::{dsl, table}; /// defined in the backend_config field. A single channel may be attached to /// (in other words, "enabled" or "selected" for) any number of projects within /// the same team. -#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)] -#[diesel(belongs_to(Team))] +#[derive(Clone, Debug, Identifiable, Queryable, Selectable)] #[diesel(check_for_backend(Pg))] pub struct Channel { pub id: Uuid, @@ -57,6 +57,23 @@ impl Channel { pub fn where_enabled_by_default() -> _ { channels::enable_by_default.eq(true) } + + pub fn insertable_builder() -> InsertableChannelBuilder { + InsertableChannelBuilder::default() + } +} + +#[derive(Builder, Clone, Debug, Insertable)] +#[diesel(table_name = channels)] +#[builder(pattern = "owned", setter(prefix = "with"))] +pub struct InsertableChannel { + #[builder(setter(skip), default = "uuid::Uuid::now_v7()")] + id: Uuid, + team_id: Uuid, + name: String, + #[builder(setter(strip_option), default)] + enable_by_default: Option, + backend_config: BackendConfig, } // Note: In a previous implementation, channel configuration was handled by diff --git a/src/channels_router.rs b/src/channels_router.rs index 2ea4138..2f4df07 100644 --- a/src/channels_router.rs +++ b/src/channels_router.rs @@ -1,8 +1,8 @@ -use anyhow::Context as _; +use anyhow::{Context as _, Result}; use askama::Template; use axum::{ - extract::{Path, State}, - response::{Html, IntoResponse, Redirect}, + extract::{OriginalUri, Path, State}, + response::{Html, IntoResponse as _, Redirect, Response}, routing::{get, post}, Router, }; @@ -16,17 +16,16 @@ use crate::{ app_error::AppError, app_state::{AppState, DbConn, ReqwestClient}, channels::{ - BackendConfig, Channel, EmailBackendConfig, SlackBackendConfig, CHANNEL_BACKEND_EMAIL, - CHANNEL_BACKEND_SLACK, + self, BackendConfig, Channel, EmailBackendConfig, SlackBackendConfig, + CHANNEL_BACKEND_EMAIL, CHANNEL_BACKEND_SLACK, }, csrf::generate_csrf_token, email::{is_permissible_email, MailSender as _, Mailer}, guards, nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_CHANNELS}, - schema::channels, settings::{Settings, SlackSettings}, slack_auth, - slack_utils::{self, ConversationType, SlackClient}, + slack_utils::{self, ConversationType, SlackClient, SlackError}, users::CurrentUser, }; @@ -82,7 +81,7 @@ async fn channels_page( DbConn(db_conn): DbConn, Path(team_id): Path, CurrentUser(current_user): CurrentUser, -) -> Result { +) -> Result { let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; let channels = { @@ -139,41 +138,42 @@ async fn post_new_channel( Path(team_id): Path, CurrentUser(current_user): CurrentUser, Form(form_body): Form, -) -> Result { +) -> Result { let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?; - let channel_id = Uuid::now_v7(); let channel = match form_body.channel_type.as_str() { CHANNEL_BACKEND_EMAIL => db_conn - .interact::<_, Result>(move |conn| { - Ok(diesel::insert_into(channels::table) - .values(( - channels::id.eq(channel_id), - channels::team_id.eq(team_id), - channels::name.eq("Untitled Email Channel"), - channels::backend_config - .eq(Into::::into(EmailBackendConfig::default())), - )) + .interact(move |conn| -> Result { + diesel::insert_into(channels::table) + .values( + Channel::insertable_builder() + .with_team_id(team_id) + .with_name("Untitled Email Channel".to_owned()) + .with_backend_config(EmailBackendConfig::default().into()) + .build() + .context("failed to build insertable channel")?, + ) .returning(Channel::as_returning()) .get_result(conn) - .context("Failed to insert new EmailChannel.")?) + .context("Failed to insert new EmailChannel.") }) .await .unwrap()?, CHANNEL_BACKEND_SLACK => db_conn - .interact::<_, Result>(move |conn| { - Ok(diesel::insert_into(channels::table) - .values(( - channels::id.eq(channel_id), - channels::team_id.eq(team_id), - channels::name.eq("Untitled Slack Channel"), - channels::backend_config - .eq(Into::::into(SlackBackendConfig::default())), - )) + .interact(move |conn| -> Result { + diesel::insert_into(channels::table) + .values( + Channel::insertable_builder() + .with_team_id(team_id) + .with_name("Untitled Slack Channel".to_owned()) + .with_backend_config(SlackBackendConfig::default().into()) + .build() + .context("failed to build insertable channel")?, + ) .returning(Channel::as_returning()) .get_result(conn) - .context("Failed to insert new SlackChannel.")?) + .context("Failed to insert new SlackChannel.") }) .await .unwrap()?, @@ -189,7 +189,8 @@ async fn post_new_channel( base_path, team.id.simple(), channel.id.simple() - ))) + )) + .into_response()) } async fn channel_page( @@ -206,7 +207,8 @@ async fn channel_page( DbConn(db_conn): DbConn, Path((team_id, channel_id)): Path<(Uuid, Uuid)>, CurrentUser(current_user): CurrentUser, -) -> Result { + OriginalUri(original_uri): OriginalUri, +) -> Result { let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; let channel = { @@ -260,21 +262,53 @@ async fn channel_page( .build(), } .render()?, - )) + ) + .into_response()) } BackendConfig::Slack(slack_data) => { - let slack_client = slack_data.oauth_tokens.map(|tokens| { + let slack_client = slack_data.oauth_tokens.clone().map(|tokens| { SlackClient::new(&tokens.access_token) .with_reqwest_client(reqwest_client) .with_api_root(&slack_api_root) }); let slack_channels = if let Some(client) = slack_client { - client + match client .list_conversations() .with_types([ConversationType::PublicChannel]) .with_exclude_archived(true) .load_all() - .await? + .await + { + Err(SlackError::Api(slack_utils::ApiError { + error: slack_utils::ErrorCode::AccountInactive, + })) => { + // Access needs to be reauthorized. + tracing::info!("encountered account_inactive error for slack backend of channel {}; resetting oauth tokens", channel.id); + let new_slack_data = SlackBackendConfig { + oauth_tokens: None, + ..slack_data + }; + db_conn + .interact(move |conn| -> Result<()> { + diesel::update( + channels::table.filter(Channel::with_id(&channel.id)), + ) + .set( + channels::dsl::backend_config + .eq(BackendConfig::from(new_slack_data)), + ) + .execute(conn) + .context("failed to clear oauth tokens on slack backend config") + .and(Ok(())) + }) + .await + .unwrap()?; + // Have the HTTP client refresh now that the old OAuth + // tokens have been cleared. + return Ok(Redirect::to(&original_uri.to_string()).into_response()); + } + other => other, + }? } else { Vec::new() }; @@ -306,7 +340,8 @@ async fn channel_page( slack_channels, } .render()?, - )) + ) + .into_response()) } } } @@ -324,7 +359,7 @@ async fn update_channel( Path((team_id, channel_id)): Path<(Uuid, Uuid)>, CurrentUser(current_user): CurrentUser, Form(form_body): Form, -) -> Result { +) -> Result { guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?; guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; @@ -337,8 +372,8 @@ async fn update_channel( .filter(Channel::with_team(&team_id)), ) .set(( - channels::name.eq(form_body.name), - channels::enable_by_default + channels::dsl::name.eq(form_body.name), + channels::dsl::enable_by_default .eq(form_body.enable_by_default.unwrap_or("false".to_string()) == "true"), )) .execute(conn) @@ -379,7 +414,7 @@ async fn update_channel_email_recipient( Path((team_id, channel_id)): Path<(Uuid, Uuid)>, CurrentUser(current_user): CurrentUser, Form(form_body): Form, -) -> Result { +) -> Result { guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?; guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; @@ -402,14 +437,14 @@ async fn update_channel_email_recipient( // TODO: transaction retries conn.transaction::<_, AppError, _>(move |conn| { let channel = get_channel_by_params(conn, &team_id, &channel_id)?; - let new_config = BackendConfig::Email(EmailBackendConfig { + let new_config = EmailBackendConfig { recipient, verification_code, verification_code_guesses: 0, ..channel.backend_config.try_into()? - }); + }; let num_rows = diesel::update(channels::table.filter(Channel::with_id(&channel.id))) - .set(channels::backend_config.eq(new_config)) + .set(channels::dsl::backend_config.eq(BackendConfig::from(new_config))) .execute(conn)?; if num_rows != 1 { return Err(anyhow::anyhow!( @@ -447,7 +482,8 @@ async fn update_channel_email_recipient( base_path, team_id.simple(), channel_id.simple() - ))) + )) + .into_response()) } #[derive(Deserialize)] @@ -462,7 +498,7 @@ async fn update_channel_slack_conversation( Path((team_id, channel_id)): Path<(Uuid, Uuid)>, CurrentUser(current_user): CurrentUser, Form(form): Form, -) -> Result { +) -> Result { guards::require_valid_csrf_token(&form.csrf_token, ¤t_user, &db_conn).await?; guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; @@ -491,7 +527,7 @@ async fn update_channel_slack_conversation( // TODO: Ensure this holds true with private channels and groups. slack_data.conversation_id = Some(form.conversation_id); let num_rows = diesel::update(channels::table.filter(Channel::with_id(&channel.id))) - .set(channels::backend_config.eq(BackendConfig::from(slack_data))) + .set(channels::dsl::backend_config.eq(BackendConfig::from(slack_data))) .execute(conn)?; tracing::debug!("updated {} rows", num_rows); // If the channel is deleted while this db interaction is running, 0 @@ -509,7 +545,8 @@ async fn update_channel_slack_conversation( base_path, team_id.simple(), channel_id.simple() - ))) + )) + .into_response()) } #[derive(Deserialize)] @@ -524,7 +561,7 @@ async fn verify_email( Path((team_id, channel_id)): Path<(Uuid, Uuid)>, CurrentUser(current_user): CurrentUser, Form(form_body): Form, -) -> Result { +) -> Result { guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?; guards::require_team_membership(¤t_user, &team_id, &db_conn).await?; @@ -565,7 +602,7 @@ async fn verify_email( } }; diesel::update(channels::table.filter(Channel::with_id(&channel_id))) - .set(channels::backend_config.eq(Into::::into(new_config))) + .set(channels::dsl::backend_config.eq(BackendConfig::from(new_config))) .execute(conn)?; Ok(()) }) @@ -579,5 +616,6 @@ async fn verify_email( base_path, team_id.simple(), channel_id.simple() - ))) + )) + .into_response()) } diff --git a/src/csrf.rs b/src/csrf.rs index d5270be..12c305d 100644 --- a/src/csrf.rs +++ b/src/csrf.rs @@ -7,12 +7,14 @@ use diesel::{ }; use uuid::Uuid; -use crate::{app_error::AppError, schema::csrf_tokens::dsl::*}; +use crate::{app_error::AppError, schema::csrf_tokens}; + +pub use crate::schema::csrf_tokens::{dsl, table}; const TOKEN_PREFIX: &str = "csrf-"; #[derive(Clone, Debug, Identifiable, Queryable, Selectable)] -#[diesel(table_name = crate::schema::csrf_tokens)] +#[diesel(table_name = csrf_tokens)] #[diesel(check_for_backend(Pg))] pub struct CsrfToken { pub id: Uuid, @@ -21,24 +23,24 @@ pub struct CsrfToken { } impl CsrfToken { - fn all() -> Select> { - csrf_tokens.select(Self::as_select()) + fn all() -> Select> { + table.select(Self::as_select()) } - pub fn is_not_expired() -> Gt> { + pub fn is_not_expired() -> Gt> { let ttl = TimeDelta::hours(24); let min_created_at: DateTime = Utc::now() - ttl; - created_at.gt(min_created_at) + dsl::created_at.gt(min_created_at) } #[auto_type(no_type_alias)] pub fn with_user_id<'a>(token_user_id: &'a Option) -> _ { - user_id.is_not_distinct_from(token_user_id) + dsl::user_id.is_not_distinct_from(token_user_id) } #[auto_type(no_type_alias)] pub fn with_token_id<'a>(token_id: &'a Uuid) -> _ { - id.eq(token_id) + dsl::id.eq(token_id) } } @@ -50,11 +52,11 @@ pub async fn generate_csrf_token( let token_id = Uuid::new_v4(); db_conn .interact(move |conn| { - diesel::insert_into(csrf_tokens) + diesel::insert_into(table) .values(( - id.eq(token_id), - user_id.eq(with_user_id), - created_at.eq(diesel::dsl::now), + dsl::id.eq(token_id), + dsl::user_id.eq(with_user_id), + dsl::created_at.eq(diesel::dsl::now), )) .execute(conn) }) diff --git a/src/governors.rs b/src/governors.rs index e4a2808..ccacb69 100644 --- a/src/governors.rs +++ b/src/governors.rs @@ -1,9 +1,10 @@ // Fault tolerant rate limiting backed by Postgres. -use anyhow::Result; +use anyhow::{Context as _, Result}; use chrono::{DateTime, TimeDelta, Utc}; +use derive_builder::Builder; use diesel::{ - dsl::{auto_type, insert_into, AsSelect}, + dsl::{auto_type, AsSelect}, pg::Pg, prelude::*, sql_types::Timestamptz, @@ -12,12 +13,14 @@ use uuid::Uuid; use crate::schema::{governor_entries, governors}; +pub use crate::schema::governors::{dsl, table}; + // Expose built-in Postgres GREATEST() function to Diesel define_sql_function! { fn greatest(a: diesel::sql_types::Integer, b: diesel::sql_types::Integer) -> Integer } -#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)] +#[derive(Clone, Debug, Identifiable, Queryable, Selectable)] #[diesel(table_name = governors)] pub struct Governor { pub id: Uuid, @@ -29,75 +32,86 @@ pub struct Governor { } impl Governor { - pub fn insert_new<'a>( - db_conn: &mut diesel::PgConnection, - team_id: &'a Uuid, - project_id: Option<&'a Uuid>, - window_size: &'a TimeDelta, - max_count: i32, - ) -> Result { - let id: Uuid = Uuid::now_v7(); - Ok(insert_into(governors::table) - .values(( - governors::team_id.eq(team_id), - governors::id.eq(id), - governors::project_id.eq(project_id), - governors::window_size.eq(window_size), - governors::max_count.eq(max_count), - )) - .get_result(db_conn)?) + pub fn insertable_builder() -> InsertableGovernorBuilder { + InsertableGovernorBuilder::default() + } + + pub fn lazy_getter() -> LazyGetterBuilder { + LazyGetterBuilder::default() } #[auto_type(no_type_alias)] pub fn all() -> _ { let select: AsSelect = Governor::as_select(); - governors::table.select(select) + table.select(select) } #[auto_type(no_type_alias)] pub fn with_id<'a>(governor_id: &'a Uuid) -> _ { - governors::id.eq(governor_id) + dsl::id.eq(governor_id) } #[auto_type(no_type_alias)] pub fn with_team<'a>(team_id: &'a Uuid) -> _ { - governors::team_id.eq(team_id) + dsl::team_id.eq(team_id) } #[auto_type(no_type_alias)] pub fn with_project<'a>(project_id: &'a Option) -> _ { - governors::project_id.is_not_distinct_from(project_id) + dsl::project_id.is_not_distinct_from(project_id) } - // TODO: return a custom result enum instead of a Result