/* * Author: Ruben Fiszel * Copyright: Windmill Labs, Inc 2022 * This file and its contents are licensed under the AGPLv3 License. * Please see the included NOTICE for copyright information and * LICENSE-AGPL for a copy of the license. */ use std::{ fmt::{self, Display}, hash::{Hash, Hasher}, }; use crate::{ error::{to_anyhow, Error}, utils::http_get_from_hub, DB, HUB_BASE_URL, }; use serde::de::Error as _; use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize}; use serde_json::to_string_pretty; use crate::utils::StripPath; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash, Eq, sqlx::Type)] #[sqlx(type_name = "SCRIPT_LANG", rename_all = "lowercase")] #[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] pub enum ScriptLang { Nativets, Deno, Python3, Go, Bash, Powershell, Postgresql, Bun, Mysql, Bigquery, Snowflake, Graphql, Mssql, } impl ScriptLang { pub fn as_str(&self) -> &'static str { match self { ScriptLang::Bun => "bun", ScriptLang::Nativets => "nativets", ScriptLang::Deno => "deno", ScriptLang::Python3 => "python3", ScriptLang::Go => "go", ScriptLang::Bash => "bash", ScriptLang::Powershell => "powershell", ScriptLang::Postgresql => "postgresql", ScriptLang::Mysql => "mysql", ScriptLang::Bigquery => "bigquery", ScriptLang::Snowflake => "snowflake", ScriptLang::Mssql => "mssql", ScriptLang::Graphql => "graphql", } } } #[derive(PartialEq, Debug, Hash, Clone, Copy, sqlx::Type)] #[sqlx(transparent)] pub struct ScriptHash(pub i64); #[derive(PartialEq, sqlx::Type)] #[sqlx(transparent, no_pg_array)] pub struct ScriptHashes(pub Vec); impl Display for ScriptHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", to_hex_string(&self.0)) } } impl Serialize for ScriptHash { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { serializer.serialize_str(to_hex_string(&self.0).as_str()) } } impl<'de> Deserialize<'de> for ScriptHash { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; let i = to_i64(&s).map_err(|e| D::Error::custom(format!("{}", e)))?; Ok(ScriptHash(i)) } } impl Serialize for ScriptHashes { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { let mut seq = serializer.serialize_seq(Some(self.0.len()))?; for element in &self.0 { seq.serialize_element(&ScriptHash(*element))?; } seq.end() } } #[derive(Serialize, Deserialize, Debug, Hash, sqlx::Type)] #[sqlx(type_name = "SCRIPT_KIND", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum ScriptKind { Trigger, Failure, Script, Approval, } impl Display for ScriptKind { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { ScriptKind::Trigger => "trigger", ScriptKind::Failure => "failure", ScriptKind::Script => "script", ScriptKind::Approval => "approval", })?; Ok(()) } } #[derive(Serialize, sqlx::FromRow)] pub struct Script { pub workspace_id: String, pub hash: ScriptHash, pub path: String, pub parent_hashes: Option, pub summary: String, pub description: String, pub content: String, pub created_by: String, pub created_at: chrono::DateTime, pub archived: bool, pub schema: Option, pub deleted: bool, pub is_template: bool, pub extra_perms: serde_json::Value, pub lock: Option, pub lock_error_logs: Option, pub language: ScriptLang, pub kind: ScriptKind, pub tag: Option, #[serde(skip_serializing_if = "Option::is_none")] pub draft_only: Option, #[serde(skip_serializing_if = "Option::is_none")] pub envs: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub concurrent_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub concurrency_time_window_s: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dedicated_worker: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_ttl: Option, #[serde(skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(skip_serializing_if = "Option::is_none")] pub delete_after_use: Option, #[serde(skip_serializing_if = "Option::is_none")] pub restart_unless_cancelled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub concurrency_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, #[serde(skip_serializing_if = "Option::is_none")] pub no_main_func: Option, } #[derive(Serialize, sqlx::FromRow)] pub struct ListableScript { pub hash: ScriptHash, pub path: String, pub summary: String, pub created_at: chrono::DateTime, pub archived: bool, pub extra_perms: serde_json::Value, pub language: ScriptLang, pub starred: bool, pub tag: Option, #[serde(skip_serializing_if = "Option::is_none")] pub has_draft: Option, #[serde(skip_serializing_if = "Option::is_none")] pub draft_only: Option, pub has_deploy_errors: bool, pub ws_error_handler_muted: Option, #[serde(skip_serializing_if = "Option::is_none")] pub no_main_func: Option, } #[derive(Serialize)] pub struct ScriptHistory { pub script_hash: ScriptHash, #[serde(skip_serializing_if = "Option::is_none")] pub deployment_msg: Option, } #[derive(Deserialize)] pub struct ScriptHistoryUpdate { pub deployment_msg: Option, } #[derive(Serialize, Deserialize, Debug, sqlx::Type)] #[sqlx(transparent)] #[serde(transparent)] pub struct Schema(pub serde_json::Value); impl Hash for Schema { fn hash(&self, state: &mut H) { if let Ok(s) = to_string_pretty(&self.0) { s.hash(state); } } } #[derive(Serialize, Deserialize, Hash)] pub struct NewScript { pub path: String, pub parent_hash: Option, pub summary: String, pub description: String, pub content: String, pub schema: Option, pub is_template: Option, #[serde(default = "Option::default")] #[serde(deserialize_with = "lock_deserialize")] pub lock: Option, pub language: ScriptLang, pub kind: Option, pub tag: Option, pub draft_only: Option, pub envs: Option>, pub concurrent_limit: Option, pub concurrency_time_window_s: Option, pub cache_ttl: Option, pub dedicated_worker: Option, pub ws_error_handler_muted: Option, pub priority: Option, pub timeout: Option, pub delete_after_use: Option, pub restart_unless_cancelled: Option, pub deployment_message: Option, #[serde(skip_serializing_if = "Option::is_none")] pub concurrency_key: Option, pub visible_to_runner_only: Option, pub no_main_func: Option, } fn lock_deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::de::Deserializer<'de>, { struct StringOrArrayVisitor; impl<'de> serde::de::Visitor<'de> for StringOrArrayVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("either a string or an array of strings") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { Ok(Some(v.to_string())) } fn visit_none(self) -> Result where E: serde::de::Error, { Ok(None) } fn visit_unit(self) -> Result where E: serde::de::Error, { Ok(None) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut split_lock: Vec = vec![]; loop { if let Ok(Some(elem)) = seq.next_element::() { split_lock.push(elem); } else { break; } } let lock = split_lock.join("\n"); return Ok(Some(lock)); } } deserializer.deserialize_any(StringOrArrayVisitor) } #[derive(Deserialize)] pub struct ListScriptQuery { pub path_start: Option, pub path_exact: Option, pub created_by: Option, pub first_parent_hash: Option, pub last_parent_hash: Option, pub parent_hash: Option, pub show_archived: Option, pub order_by: Option, pub order_desc: Option, pub is_template: Option, pub kinds: Option, pub starred_only: Option, pub hide_without_main: Option, } pub fn to_i64(s: &str) -> crate::error::Result { let v = hex::decode(s)?; if v.len() < 8 { return Err(crate::error::Error::BadRequest(format!( "hex string did not decode to an u64: {s}", ))); } let nb: u64 = u64::from_be_bytes( v[0..8] .try_into() .map_err(|_| hex::FromHexError::InvalidStringLength)?, ); Ok(nb as i64) } pub fn to_hex_string(i: &i64) -> String { hex::encode(i.to_be_bytes()) } pub async fn get_hub_script_by_path( path: StripPath, http_client: &reqwest::Client, db: &DB, ) -> crate::error::Result { let path = path .to_path() .strip_prefix("hub/") .ok_or_else(|| Error::BadRequest("Impossible to remove prefix hex".to_string()))?; let content = http_get_from_hub( http_client, &format!("{}/raw/{}.ts", *HUB_BASE_URL.read().await, path), true, None, db, ) .await? .text() .await .map_err(to_anyhow)?; Ok(content) } pub async fn get_full_hub_script_by_path( path: StripPath, http_client: &reqwest::Client, db: &DB, ) -> crate::error::Result { let path = path .to_path() .strip_prefix("hub/") .ok_or_else(|| Error::BadRequest("Impossible to remove prefix hex".to_string()))?; let value = http_get_from_hub( http_client, &format!("{}/raw2/{}", *HUB_BASE_URL.read().await, path), true, None, db, ) .await? .json::() .await .map_err(to_anyhow)?; Ok(value) } #[derive(Deserialize, Serialize)] pub struct HubScript { pub content: String, pub lockfile: Option, pub language: ScriptLang, pub schema: serde_json::Value, pub summary: Option, }