From 13e2e32ca521196ef2e1e2da99b265b5f98bb159 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 7 Jun 2021 00:32:15 -0400 Subject: [PATCH] wip on moderation of templates --- sqlx-data.json | 401 +++++++++--------- src/common_queries/mod.rs | 1 + src/common_queries/query_w2l_template.rs | 182 ++++++++ .../endpoints/w2l/get_w2l_template.rs | 2 + src/http_server/endpoints/w2l/mod.rs | 3 +- .../w2l/set_w2l_template_mod_approval.rs | 169 ++++++++ src/tts_download_job.rs | 1 + src/tts_inference_job.rs | 1 + src/w2l_download_job.rs | 1 + src/w2l_inference_job.rs | 1 + 10 files changed, 567 insertions(+), 195 deletions(-) create mode 100644 src/common_queries/query_w2l_template.rs create mode 100644 src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs diff --git a/sqlx-data.json b/sqlx-data.json index 25140e939..7cb6849be 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -490,6 +490,213 @@ ] } }, + "1c35b51bf807a3cd27b9deef2278c066626215e631c651cabb0fff56675f3624": { + "query": "\nSELECT\n w2l.token as template_token,\n w2l.template_type,\n w2l.creator_user_token,\n users.username as creator_username,\n users.display_name as creator_display_name,\n w2l.updatable_slug,\n w2l.title,\n w2l.frame_width,\n w2l.frame_height,\n w2l.duration_millis,\n w2l.maybe_public_bucket_preview_image_object_name,\n w2l.maybe_public_bucket_preview_video_object_name,\n w2l.is_mod_approved,\n w2l.created_at,\n w2l.updated_at\nFROM w2l_templates as w2l\nJOIN users\nON users.token = w2l.creator_user_token\nWHERE w2l.updatable_slug = ?\nAND w2l.deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "template_token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "template_type", + "type_info": { + "type": "String", + "flags": { + "bits": 393 + }, + "char_set": 224, + "max_size": 28 + } + }, + { + "ordinal": 2, + "name": "creator_user_token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4233 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 3, + "name": "creator_username", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "creator_display_name", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4225 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 5, + "name": "updatable_slug", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 256 + } + }, + { + "ordinal": 6, + "name": "title", + "type_info": { + "type": "String", + "flags": { + "bits": 4225 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 7, + "name": "frame_width", + "type_info": { + "type": "Long", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 10 + } + }, + { + "ordinal": 8, + "name": "frame_height", + "type_info": { + "type": "Long", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 10 + } + }, + { + "ordinal": 9, + "name": "duration_millis", + "type_info": { + "type": "Long", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 10 + } + }, + { + "ordinal": 10, + "name": "maybe_public_bucket_preview_image_object_name", + "type_info": { + "type": "VarString", + "flags": { + "bits": 128 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "maybe_public_bucket_preview_video_object_name", + "type_info": { + "type": "VarString", + "flags": { + "bits": 128 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 12, + "name": "is_mod_approved", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 13, + "name": "created_at", + "type_info": { + "type": "Timestamp", + "flags": { + "bits": 1153 + }, + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 14, + "name": "updated_at", + "type_info": { + "type": "Timestamp", + "flags": { + "bits": 9345 + }, + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false, + false + ] + } + }, "215f82f4034ccb47bfa000c3fead66c3c9f7d5caa905aa99a702d396aeeebbc6": { "query": "\nINSERT INTO tts_model_upload_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n creator_user_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n title = ?,\n tts_model_type = ?,\n maybe_subject_token = ?,\n maybe_actor_subject_token = ?,\n download_url = ?,\n download_url_type = ?,\n status = \"pending\"\n ", "describe": { @@ -2034,200 +2241,6 @@ ] } }, - "4995b1688dab1a25fa16ea59d6282b052be5c4ed6bf740a0840e24c9ad67e55a": { - "query": "\nSELECT\n w2l.token as template_token,\n w2l.template_type,\n w2l.creator_user_token,\n users.username as creator_username,\n users.display_name as creator_display_name,\n w2l.updatable_slug,\n w2l.title,\n w2l.frame_width,\n w2l.frame_height,\n w2l.duration_millis,\n w2l.maybe_public_bucket_preview_image_object_name,\n w2l.maybe_public_bucket_preview_video_object_name,\n w2l.created_at,\n w2l.updated_at\nFROM w2l_templates as w2l\nJOIN users\nON users.token = w2l.creator_user_token\nWHERE w2l.updatable_slug = ?\nAND w2l.deleted_at IS NULL\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "template_token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "template_type", - "type_info": { - "type": "String", - "flags": { - "bits": 393 - }, - "char_set": 224, - "max_size": 28 - } - }, - { - "ordinal": 2, - "name": "creator_user_token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4233 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 3, - "name": "creator_username", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 4, - "name": "creator_display_name", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4225 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 5, - "name": "updatable_slug", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 256 - } - }, - { - "ordinal": 6, - "name": "title", - "type_info": { - "type": "String", - "flags": { - "bits": 4225 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 7, - "name": "frame_width", - "type_info": { - "type": "Long", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 10 - } - }, - { - "ordinal": 8, - "name": "frame_height", - "type_info": { - "type": "Long", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 10 - } - }, - { - "ordinal": 9, - "name": "duration_millis", - "type_info": { - "type": "Long", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 10 - } - }, - { - "ordinal": 10, - "name": "maybe_public_bucket_preview_image_object_name", - "type_info": { - "type": "VarString", - "flags": { - "bits": 128 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 11, - "name": "maybe_public_bucket_preview_video_object_name", - "type_info": { - "type": "VarString", - "flags": { - "bits": 128 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 12, - "name": "created_at", - "type_info": { - "type": "Timestamp", - "flags": { - "bits": 1153 - }, - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 13, - "name": "updated_at", - "type_info": { - "type": "Timestamp", - "flags": { - "bits": 9345 - }, - "char_set": 63, - "max_size": 19 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - false - ] - } - }, "4b4b6c2b8306cd5b6b1fe28fd492de5e346f0a53c0efc4f0eec57c4238860764": { "query": "\nSELECT\n tts_results.token as tts_result_token,\n\n tts_results.model_token as tts_model_token,\n tts_results.inference_text as inference_text,\n\n users.token as maybe_creator_user_token,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n\n tts_results.file_size_bytes,\n tts_results.duration_millis,\n tts_results.created_at,\n tts_results.updated_at\n\nFROM tts_results\nLEFT OUTER JOIN tts_models\n ON tts_results.model_token = tts_models.token\nLEFT OUTER JOIN users\n ON tts_results.maybe_creator_user_token = users.token\nWHERE\n tts_results.deleted_at IS NULL\n AND tts_results.is_mod_hidden_from_public IS FALSE\n AND tts_models.is_mod_disabled IS FALSE\n ", "describe": { diff --git a/src/common_queries/mod.rs b/src/common_queries/mod.rs index 30aab5e19..8feffdc37 100644 --- a/src/common_queries/mod.rs +++ b/src/common_queries/mod.rs @@ -5,4 +5,5 @@ pub mod list_tts_models; pub mod list_w2l_inference_results; pub mod list_w2l_templates; pub mod query_user_profile; +pub mod query_w2l_template; pub mod sessions; \ No newline at end of file diff --git a/src/common_queries/query_w2l_template.rs b/src/common_queries/query_w2l_template.rs new file mode 100644 index 000000000..186a04a00 --- /dev/null +++ b/src/common_queries/query_w2l_template.rs @@ -0,0 +1,182 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use crate::AnyhowResult; +use crate::database_helpers::boolean_converters::nullable_i8_to_optional_bool; +use crate::database_helpers::boolean_converters::i8_to_bool; +use derive_more::{Display, Error}; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::MySqlPool; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::sync::Arc; + +// TODO: This duplicates the get_w2l_template_handler. + +#[derive(Serialize)] +pub struct W2lTemplateRecordForResponse { + pub template_token: String, + pub template_type: String, + pub creator_user_token: String, + pub creator_username: String, + pub creator_display_name: String, + pub updatable_slug: String, + pub title: String, + pub frame_width: u32, + pub frame_height: u32, + pub duration_millis: u32, + pub maybe_image_object_name: Option, + pub maybe_video_object_name: Option, + pub is_mod_public_listing_approved: Option, + pub is_mod_disabled: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, +} + +#[derive(Serialize)] +pub struct W2lTemplateRecordRaw { + pub template_token: String, + pub template_type: String, + pub creator_user_token: String, + pub creator_username: String, + pub creator_display_name: String, + pub updatable_slug: String, + pub title: String, + pub frame_width: i32, + pub frame_height: i32, + pub duration_millis: i32, + pub maybe_public_bucket_preview_image_object_name: Option, + pub maybe_public_bucket_preview_video_object_name: Option, + pub is_mod_public_listing_approved: Option, + pub is_mod_disabled: i8, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, +} + + +pub async fn select_w2l_template_by_token( + w2l_template_token: &str, + can_see_deleted: bool, + mysql_pool: &MySqlPool +) -> AnyhowResult> { + + let maybe_record = if can_see_deleted { + select_including_deleted(w2l_template_token, mysql_pool).await + } else { + select_without_deleted(w2l_template_token, mysql_pool).await + }; + + let template : W2lTemplateRecordRaw = match maybe_record { + Ok(template) => template, + Err(ref err) => { + match err { + RowNotFound => { + warn!("w2l template not found: {:?}", &err); + return Ok(None); + }, + _ => { + warn!("w2l template query error: {:?}", &err); + return Err(anyhow!("database error")); + } + } + } + }; + + let template_for_response = W2lTemplateRecordForResponse { + template_token: template.template_token.clone(), + template_type: template.template_type.clone(), + creator_user_token: template.creator_user_token.clone(), + creator_username: template.creator_username.clone(), + creator_display_name: template.creator_display_name.clone(), + updatable_slug: template.updatable_slug.clone(), + title: template.title.clone(), + frame_width: if template.frame_width > 0 { template.frame_width as u32 } else { 0 }, + frame_height: if template.frame_height > 0 { template.frame_height as u32 } else { 0 }, + duration_millis: if template.duration_millis > 0 { template.duration_millis as u32 } else { 0 }, + maybe_image_object_name: template.maybe_public_bucket_preview_image_object_name.clone(), + maybe_video_object_name: template.maybe_public_bucket_preview_video_object_name.clone(), + is_mod_public_listing_approved: nullable_i8_to_optional_bool(template.is_mod_public_listing_approved), + is_mod_disabled: i8_to_bool(template.is_mod_disabled), + created_at: template.created_at.clone(), + updated_at: template.updated_at.clone(), + deleted_at: template.deleted_at.clone(), + }; + + Ok(Some(template_for_response)) +} + +async fn select_including_deleted( + w2l_template_token: &str, + mysql_pool: &MySqlPool +) -> Result { + sqlx::query_as!( + W2lTemplateRecordRaw, + r#" +SELECT + w2l.token as template_token, + w2l.template_type, + w2l.creator_user_token, + users.username as creator_username, + users.display_name as creator_display_name, + w2l.updatable_slug, + w2l.title, + w2l.frame_width, + w2l.frame_height, + w2l.duration_millis, + w2l.maybe_public_bucket_preview_image_object_name, + w2l.maybe_public_bucket_preview_video_object_name, + w2l.is_mod_approved as is_mod_public_listing_approved, + w2l.is_mod_disabled, + w2l.created_at, + w2l.updated_at, + w2l.deleted_at +FROM w2l_templates as w2l +JOIN users +ON users.token = w2l.creator_user_token +WHERE w2l.token = ? + "#, + w2l_template_token + ) + .fetch_one(mysql_pool) + .await // TODO: This will return error if it doesn't exist +} + +async fn select_without_deleted( + w2l_template_token: &str, + mysql_pool: &MySqlPool +) -> Result { + sqlx::query_as!( + W2lTemplateRecordRaw, + r#" +SELECT + w2l.token as template_token, + w2l.template_type, + w2l.creator_user_token, + users.username as creator_username, + users.display_name as creator_display_name, + w2l.updatable_slug, + w2l.title, + w2l.frame_width, + w2l.frame_height, + w2l.duration_millis, + w2l.maybe_public_bucket_preview_image_object_name, + w2l.maybe_public_bucket_preview_video_object_name, + w2l.is_mod_approved as is_mod_public_listing_approved, + w2l.is_mod_disabled, + w2l.created_at, + w2l.updated_at, + w2l.deleted_at +FROM w2l_templates as w2l +JOIN users +ON users.token = w2l.creator_user_token +WHERE w2l.token = ? +AND w2l.deleted_at IS NULL + "#, + w2l_template_token + ) + .fetch_one(mysql_pool) + .await // TODO: This will return error if it doesn't exist +} diff --git a/src/http_server/endpoints/w2l/get_w2l_template.rs b/src/http_server/endpoints/w2l/get_w2l_template.rs index 6cf51fb76..740ef51ea 100644 --- a/src/http_server/endpoints/w2l/get_w2l_template.rs +++ b/src/http_server/endpoints/w2l/get_w2l_template.rs @@ -27,6 +27,8 @@ use sqlx::mysql::MySqlDatabaseError; use std::sync::Arc; use crate::database_helpers::boolean_converters::nullable_i8_to_optional_bool; +// TODO: This is duplicated in query_w2l_template + /// For the URL PathInfo #[derive(Deserialize)] pub struct GetW2lTemplatePathInfo { diff --git a/src/http_server/endpoints/w2l/mod.rs b/src/http_server/endpoints/w2l/mod.rs index 6d0065f8d..56b37f8cf 100644 --- a/src/http_server/endpoints/w2l/mod.rs +++ b/src/http_server/endpoints/w2l/mod.rs @@ -5,4 +5,5 @@ pub mod get_w2l_inference_job_status; pub mod get_w2l_result; pub mod get_w2l_template; pub mod get_w2l_upload_template_job_status; -pub mod list_w2l_templates; \ No newline at end of file +pub mod list_w2l_templates; +pub mod set_w2l_template_mod_approval; \ No newline at end of file diff --git a/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs b/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs new file mode 100644 index 000000000..7489bc35f --- /dev/null +++ b/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs @@ -0,0 +1,169 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::cookie::Cookie; +use actix_web::dev::HttpResponseBuilder; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Path; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use crate::common_queries::query_w2l_template::select_w2l_template_by_token; +use crate::database_helpers::enums::{DownloadUrlType, CreatorSetVisibility, W2lTemplateType}; +use crate::http_server::web_utils::ip_address::get_request_ip; +use crate::server_state::ServerState; +use crate::util::random_crockford_token::random_crockford_token; +use crate::validations::model_uploads::validate_model_title; +use crate::validations::passwords::validate_passwords; +use crate::validations::username::validate_username; +use derive_more::{Display, Error}; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::sync::Arc; + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct SetW2lTemplateModApprovalPathInfo { + w2l_template_token: String, +} + +#[derive(Deserialize)] +pub struct SetW2lTemplateModApprovalRequest { + is_approved: bool, +} + +#[derive(Serialize)] +pub struct SetW2lTemplateModApprovalSuccessResponse { + pub success: bool, +} + +#[derive(Serialize)] +pub struct ErrorResponse { + pub success: bool, + pub error_reason: String, +} + +#[derive(Debug, Display)] +pub enum SetW2lTemplateModApprovalError { + BadInput(String), + NotAuthorized, + NotFound, + ServerError, +} + +impl ResponseError for SetW2lTemplateModApprovalError { + fn status_code(&self) -> StatusCode { + match *self { + SetW2lTemplateModApprovalError::BadInput(_) => StatusCode::BAD_REQUEST, + SetW2lTemplateModApprovalError::NotAuthorized => StatusCode::UNAUTHORIZED, + SetW2lTemplateModApprovalError::NotFound => StatusCode::NOT_FOUND, + SetW2lTemplateModApprovalError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + SetW2lTemplateModApprovalError::BadInput(reason) => reason.to_string(), + SetW2lTemplateModApprovalError::NotAuthorized => "unauthorized".to_string(), + SetW2lTemplateModApprovalError::NotFound => "not found".to_string(), + SetW2lTemplateModApprovalError::ServerError => "server error".to_string(), + }; + + let response = ErrorResponse { + success: false, + error_reason, + }; + + let body = match serde_json::to_string(&response) { + Ok(json) => json, + Err(_) => "{}".to_string(), + }; + + HttpResponseBuilder::new(self.status_code()) + .set_header(header::CONTENT_TYPE, "application/json") + .body(body) + } +} + +pub async fn set_w2l_template_mod_approval_handler( + http_request: HttpRequest, + path: Path, + request: web::Json, + server_state: web::Data>) -> Result +{ + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + SetW2lTemplateModApprovalError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(SetW2lTemplateModApprovalError::NotAuthorized); + } + }; + + if !user_session.can_approve_w2l_templates { + warn!("user is not allowed to approve templates: {}", user_session.user_token); + return Err(SetW2lTemplateModApprovalError::NotAuthorized); + } + + let template_query_result = select_w2l_template_by_token( + &path.w2l_template_token, + true, // Only mods can perform this action + &server_state.mysql_pool, + ).await; + + let w2l_template = match template_query_result { + Err(e) => { + warn!("query error: {:?}", e); + return Err(SetW2lTemplateModApprovalError::ServerError); + } + Ok(None) => return Err(SetW2lTemplateModApprovalError::NotFound), + Ok(Some(template)) => template, + }; + + let ip_address = get_request_ip(&http_request); + + let query_result = sqlx::query!( + r#" +UPDATE w2l_templates +SET + is_mod_approved = ?, + maybe_mod_user_token = ? +WHERE + token = ? +LIMIT 1 + "#, + + &request.is_approved, + &user_session.user_token, + &path.w2l_template_token, + ) + .execute(&server_state.mysql_pool) + .await; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update w2l mod approval status DB error: {:?}", err); + return Err(SetW2lTemplateModApprovalError::ServerError); + } + }; + let response = SetW2lTemplateModApprovalSuccessResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| SetW2lTemplateModApprovalError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/src/tts_download_job.rs b/src/tts_download_job.rs index ff567ccd4..1bb77723b 100644 --- a/src/tts_download_job.rs +++ b/src/tts_download_job.rs @@ -10,6 +10,7 @@ pub mod buckets; pub mod common_queries; +pub mod database_helpers; pub mod job_queries; pub mod script_execution; pub mod shared_constants; diff --git a/src/tts_inference_job.rs b/src/tts_inference_job.rs index efbfa34f1..f56931d6f 100644 --- a/src/tts_inference_job.rs +++ b/src/tts_inference_job.rs @@ -10,6 +10,7 @@ pub mod buckets; pub mod common_queries; +pub mod database_helpers; pub mod job_queries; pub mod script_execution; pub mod shared_constants; diff --git a/src/w2l_download_job.rs b/src/w2l_download_job.rs index af32c4a12..9a909812d 100644 --- a/src/w2l_download_job.rs +++ b/src/w2l_download_job.rs @@ -10,6 +10,7 @@ pub mod buckets; pub mod common_queries; +pub mod database_helpers; pub mod job_queries; pub mod script_execution; pub mod shared_constants; diff --git a/src/w2l_inference_job.rs b/src/w2l_inference_job.rs index 43be56ed5..fa90615c4 100644 --- a/src/w2l_inference_job.rs +++ b/src/w2l_inference_job.rs @@ -10,6 +10,7 @@ pub mod buckets; pub mod common_queries; +pub mod database_helpers; pub mod job_queries; pub mod script_execution; pub mod shared_constants;