From fb80933c8a3c8239afe7a329b37747bb32c03291 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 19 Nov 2024 03:15:03 +0000 Subject: [PATCH] rewrite/refactor settings to be more secure and capable of being embedded into templates --- Cargo.lock | 1 - core/rust.module_settings/Cargo.toml | 1 - .../src/canonical_types.rs | 529 ------------- core/rust.module_settings/src/cfg.rs | 700 ++--------------- .../src/common_columns.rs | 50 +- core/rust.module_settings/src/data_stores.rs | 740 ------------------ core/rust.module_settings/src/lib.rs | 2 - core/rust.module_settings/src/types.rs | 491 +++--------- core/rust.std/src/value.rs | 35 +- 9 files changed, 224 insertions(+), 2325 deletions(-) delete mode 100644 core/rust.module_settings/src/canonical_types.rs delete mode 100644 core/rust.module_settings/src/data_stores.rs diff --git a/Cargo.lock b/Cargo.lock index f0e4bd78..6100ee75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2837,7 +2837,6 @@ dependencies = [ "botox", "chrono", "futures-util", - "governor", "indexmap", "moka", "reqwest", diff --git a/core/rust.module_settings/Cargo.toml b/core/rust.module_settings/Cargo.toml index c181d188..5ad85403 100644 --- a/core/rust.module_settings/Cargo.toml +++ b/core/rust.module_settings/Cargo.toml @@ -14,7 +14,6 @@ serde_json = "1.0" futures-util = "0.3" indexmap = { version = "2", features = ["serde"] } moka = { version = "0.12", features = ["future", "futures-util"] } -governor = "0.6" # Anti-Raid specific splashcore_rs = { path = "../rust.std" } diff --git a/core/rust.module_settings/src/canonical_types.rs b/core/rust.module_settings/src/canonical_types.rs deleted file mode 100644 index bb951748..00000000 --- a/core/rust.module_settings/src/canonical_types.rs +++ /dev/null @@ -1,529 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum CanonicalSettingsError { - /// Operation not supported - OperationNotSupported { - operation: CanonicalOperationType, - }, - /// Generic error - Generic { - message: String, - src: String, - typ: String, - }, - /// Schema type validation error - SchemaTypeValidationError { - column: String, - expected_type: String, - got_type: String, - }, - /// Schema null value validation error - SchemaNullValueValidationError { - column: String, - }, - /// Schema check validation error - SchemaCheckValidationError { - column: String, - check: String, - error: String, - accepted_range: String, - }, - /// Missing or invalid field - MissingOrInvalidField { - field: String, - src: String, - }, - RowExists { - column_id: String, - count: i64, - }, - RowDoesNotExist { - column_id: String, - }, - MaximumCountReached { - max: usize, - current: usize, - }, -} - -impl From for CanonicalSettingsError { - fn from(error: super::types::SettingsError) -> Self { - match error { - super::types::SettingsError::OperationNotSupported { operation } => { - CanonicalSettingsError::OperationNotSupported { - operation: operation.into(), - } - } - super::types::SettingsError::Generic { message, src, typ } => { - CanonicalSettingsError::Generic { - message: message.to_string(), - src: src.to_string(), - typ: typ.to_string(), - } - } - super::types::SettingsError::SchemaTypeValidationError { - column, - expected_type, - got_type, - } => CanonicalSettingsError::SchemaTypeValidationError { - column: column.to_string(), - expected_type: expected_type.to_string(), - got_type: got_type.to_string(), - }, - super::types::SettingsError::SchemaNullValueValidationError { column } => { - CanonicalSettingsError::SchemaNullValueValidationError { - column: column.to_string(), - } - } - super::types::SettingsError::SchemaCheckValidationError { - column, - check, - error, - accepted_range, - } => CanonicalSettingsError::SchemaCheckValidationError { - column: column.to_string(), - check: check.to_string(), - error: error.to_string(), - accepted_range: accepted_range.to_string(), - }, - super::types::SettingsError::MissingOrInvalidField { field, src } => { - CanonicalSettingsError::MissingOrInvalidField { - field: field.to_string(), - src: src.to_string(), - } - } - super::types::SettingsError::RowExists { column_id, count } => { - CanonicalSettingsError::RowExists { - column_id: column_id.to_string(), - count, - } - } - super::types::SettingsError::RowDoesNotExist { column_id } => { - CanonicalSettingsError::RowDoesNotExist { - column_id: column_id.to_string(), - } - } - super::types::SettingsError::MaximumCountReached { max, current } => { - CanonicalSettingsError::MaximumCountReached { max, current } - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[allow(dead_code)] -pub enum CanonicalColumnType { - /// A single valued column (scalar) - Scalar { - /// The value type - column_type: CanonicalInnerColumnType, - }, - /// An array column - Array { - /// The inner type of the array - inner: CanonicalInnerColumnType, - }, -} - -impl From for CanonicalColumnType { - fn from(column_type: super::types::ColumnType) -> Self { - match column_type { - super::types::ColumnType::Scalar { column_type } => CanonicalColumnType::Scalar { - column_type: column_type.into(), - }, - super::types::ColumnType::Array { inner } => CanonicalColumnType::Array { - inner: inner.into(), - }, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[allow(dead_code)] -pub enum CanonicalInnerColumnTypeStringKind { - /// Normal string - Normal {}, - /// A token that is autogenerated if not provided by the user - Token { - /// The default length of the secret if not provided by the user - default_length: usize, - }, - /// A textarea - Textarea { ctx: String }, - /// A reference to a template by name - TemplateRef { - /// The kind of template - kind: String, - /// The context type to use - ctx: String, - }, - /// A kittycat permission - KittycatPermission {}, - /// User - User {}, - /// Role - Role {}, - /// Emoji - Emoji {}, - /// Message - Message {}, - /// Modifier - Modifier {}, -} - -impl From for CanonicalInnerColumnTypeStringKind { - fn from(kind: super::types::InnerColumnTypeStringKind) -> Self { - match kind { - super::types::InnerColumnTypeStringKind::Normal => { - CanonicalInnerColumnTypeStringKind::Normal {} - } - super::types::InnerColumnTypeStringKind::Token { default_length } => { - CanonicalInnerColumnTypeStringKind::Token { default_length } - } - super::types::InnerColumnTypeStringKind::Textarea { ctx } => { - CanonicalInnerColumnTypeStringKind::Textarea { - ctx: ctx.to_string(), - } - } - super::types::InnerColumnTypeStringKind::TemplateRef { kind, ctx } => { - CanonicalInnerColumnTypeStringKind::TemplateRef { - kind: kind.to_string(), - ctx: ctx.to_string(), - } - } - super::types::InnerColumnTypeStringKind::KittycatPermission => { - CanonicalInnerColumnTypeStringKind::KittycatPermission {} - } - super::types::InnerColumnTypeStringKind::User => { - CanonicalInnerColumnTypeStringKind::User {} - } - super::types::InnerColumnTypeStringKind::Role => { - CanonicalInnerColumnTypeStringKind::Role {} - } - super::types::InnerColumnTypeStringKind::Emoji => { - CanonicalInnerColumnTypeStringKind::Emoji {} - } - super::types::InnerColumnTypeStringKind::Message => { - CanonicalInnerColumnTypeStringKind::Message {} - } - super::types::InnerColumnTypeStringKind::Modifier => { - CanonicalInnerColumnTypeStringKind::Modifier {} - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[allow(dead_code)] -pub enum CanonicalInnerColumnType { - Uuid {}, - String { - min_length: Option, - max_length: Option, - allowed_values: Vec, - kind: CanonicalInnerColumnTypeStringKind, - }, - Timestamp {}, - TimestampTz {}, - Interval {}, - Integer {}, - Float {}, - BitFlag { - /// The bit flag values - values: indexmap::IndexMap, - }, - Boolean {}, - Json { - /// The maximum number of bytes for the json - max_bytes: Option, - }, -} - -impl From for CanonicalInnerColumnType { - fn from(column_type: super::types::InnerColumnType) -> Self { - match column_type { - super::types::InnerColumnType::Uuid {} => CanonicalInnerColumnType::Uuid {}, - super::types::InnerColumnType::String { - min_length, - max_length, - allowed_values, - kind, - } => CanonicalInnerColumnType::String { - min_length, - max_length, - allowed_values: allowed_values.iter().map(|s| s.to_string()).collect(), - kind: kind.into(), - }, - super::types::InnerColumnType::Timestamp {} => CanonicalInnerColumnType::Timestamp {}, - super::types::InnerColumnType::TimestampTz {} => { - CanonicalInnerColumnType::TimestampTz {} - } - super::types::InnerColumnType::Interval {} => CanonicalInnerColumnType::Interval {}, - super::types::InnerColumnType::Integer {} => CanonicalInnerColumnType::Integer {}, - super::types::InnerColumnType::Float {} => CanonicalInnerColumnType::Float {}, - super::types::InnerColumnType::BitFlag { values } => { - CanonicalInnerColumnType::BitFlag { - values: values - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect::>(), - } - } - super::types::InnerColumnType::Boolean {} => CanonicalInnerColumnType::Boolean {}, - super::types::InnerColumnType::Json { max_bytes } => { - CanonicalInnerColumnType::Json { max_bytes } - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum CanonicalColumnSuggestion { - Static { - suggestions: Vec, - }, - /// A reference to another setting - /// - /// The primary key of the referred setting is used as the value - SettingsReference { - /// The module of the referenced setting - module: String, - /// The setting of the referenced setting - setting: String, - }, - None {}, -} - -impl From for CanonicalColumnSuggestion { - fn from(column_suggestion: super::types::ColumnSuggestion) -> Self { - match column_suggestion { - super::types::ColumnSuggestion::Static { suggestions } => { - CanonicalColumnSuggestion::Static { - suggestions: suggestions.iter().map(|s| s.to_string()).collect(), - } - } - super::types::ColumnSuggestion::SettingsReference { module, setting } => { - CanonicalColumnSuggestion::SettingsReference { - module: module.to_string(), - setting: setting.to_string(), - } - } - super::types::ColumnSuggestion::None {} => CanonicalColumnSuggestion::None {}, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CanonicalColumn { - /// The ID of the column - pub id: String, - - /// The friendly name of the column - pub name: String, - - /// The description of the column - pub description: String, - - /// The type of the column - pub column_type: CanonicalColumnType, - - /// Whether or not the column is nullable - pub nullable: bool, - - /// The default value of the column - pub default: Option, - - /// Suggestions to display - pub suggestions: CanonicalColumnSuggestion, - - /// Whether or not the column is unique - pub unique: bool, - - /// For which operations should the field be ignored for (essentially, read only) - /// - /// Note that checks for this column will still be applied (use an empty array in pre_checks to disable checks) - /// - /// Semantics: - /// View => The column is removed from the list of columns sent to the consumer. The key is set to its current value when executing the actions - /// Create => The column is not handled on the client however actions are still executed. The key itself is set to None when executing the actions - /// Update => The column is not handled on the client however actions are still executed. The key itself is set to None when executing the actions - /// Delete => The column is not handled on the client however actions are still executed. The key itself is set to None when executing the actions - pub ignored_for: Vec, - - /// Whether or not the column is a secret - /// - /// Note that secret columns are not present in view or update actions unless explicitly provided by the user. ignored_for rules continue to apply. - /// - /// The exact semantics of a secret column depend on column type (a String of kind token will lead to autogeneration of a token for example) - pub secret: bool, -} - -impl From<&super::types::Column> for CanonicalColumn { - fn from(column: &super::types::Column) -> Self { - Self { - id: column.id.to_string(), - name: column.name.to_string(), - description: column.description.to_string(), - column_type: column.column_type.clone().into(), - nullable: column.nullable, - default: column.default.map(|v| v(true).to_json()), - suggestions: column.suggestions.clone().into(), - unique: column.unique, - ignored_for: column.ignored_for.iter().map(|o| (*o).into()).collect(), - secret: column.secret, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CanonicalOperationSpecific { - /// The corresponding command for ACL purposes - pub corresponding_command: String, - - /// Any columns to set. For example, a last_updated column should be set on update - /// - /// Variables: - /// - {now} => the current timestamp - /// - /// Format: {column_name} => {value} - /// - /// Note: updating columns outside of the table itself - /// - /// In Create/Update, these columns are directly included in the create/update itself - pub columns_to_set: indexmap::IndexMap, -} - -#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize, Deserialize)] -#[allow(dead_code)] -pub enum CanonicalOperationType { - #[serde(rename = "View")] - View, - #[serde(rename = "Create")] - Create, - #[serde(rename = "Update")] - Update, - #[serde(rename = "Delete")] - Delete, -} - -impl From for CanonicalOperationType { - fn from(operation_type: super::types::OperationType) -> Self { - match operation_type { - super::types::OperationType::View => CanonicalOperationType::View, - super::types::OperationType::Create => CanonicalOperationType::Create, - super::types::OperationType::Update => CanonicalOperationType::Update, - super::types::OperationType::Delete => CanonicalOperationType::Delete, - } - } -} - -impl From for super::types::OperationType { - fn from(operation_type: CanonicalOperationType) -> super::types::OperationType { - match operation_type { - CanonicalOperationType::View => super::types::OperationType::View, - CanonicalOperationType::Create => super::types::OperationType::Create, - CanonicalOperationType::Update => super::types::OperationType::Update, - CanonicalOperationType::Delete => super::types::OperationType::Delete, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CanonicalConfigOption { - /// The ID of the option - pub id: String, - - /// The name of the option - pub name: String, - - /// The description of the option - pub description: String, - - /// The table name for the config option - pub table: String, - - /// The common filters to apply to all crud operations on this config options - /// - /// For example, this can be used for guild_id scoped config options or non-guild scoped config options - /// - /// Semantics: - /// - /// View/Update/Delete: Common filters are applied to the view operation as an extension of all other filters - /// Create: Common filters are appended on to the entry itself - pub common_filters: - indexmap::IndexMap>, - - /// The default common filter - pub default_common_filters: indexmap::IndexMap, - - /// The primary key of the table - pub primary_key: String, - - /// Title template, used for the title of the embed - pub title_template: String, - - /// The columns for this option - pub columns: Vec, - - /// Maximum number of entries to return - /// - /// Only applies to View operations - pub max_return: i64, - - /// Maximum number of entries a server may have - pub max_entries: Option, - - /// Operation specific data - pub operations: indexmap::IndexMap, -} - -/// Given a module, return its canonical representation -impl From for CanonicalConfigOption { - fn from(module: super::types::ConfigOption) -> Self { - Self { - id: module.id.to_string(), - table: module.table.to_string(), - operations: module - .operations - .iter() - .map(|(k, v)| { - ((*k).into(), { - let mut columns_to_set = indexmap::IndexMap::new(); - for (k, v) in v.columns_to_set.iter() { - columns_to_set.insert(k.to_string(), v.to_string()); - } - CanonicalOperationSpecific { - corresponding_command: module.get_corresponding_command(*k), - columns_to_set, - } - }) - }) - .collect(), - common_filters: module - .common_filters - .into_iter() - .map(|(k, v)| { - ( - k.into(), - v.into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - ) - }) - .collect(), - default_common_filters: module - .default_common_filters - .into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - name: module.name.to_string(), - description: module.description.to_string(), - columns: module.columns.iter().map(|c| c.into()).collect(), - primary_key: module.primary_key.to_string(), - title_template: module.title_template.to_string(), - max_return: module.max_return, - max_entries: module.max_entries, - } - } -} diff --git a/core/rust.module_settings/src/cfg.rs b/core/rust.module_settings/src/cfg.rs index 06402b41..c1cf179e 100644 --- a/core/rust.module_settings/src/cfg.rs +++ b/core/rust.module_settings/src/cfg.rs @@ -1,3 +1,5 @@ +use crate::types::HookContext; + use super::state::State; use super::types::SettingsError; use super::types::{ @@ -13,9 +15,9 @@ fn _parse_value( column_id: &str, ) -> Result { match column_type { - ColumnType::Scalar { column_type } => { + ColumnType::Scalar { inner } => { // Special case: JSON columns can be any type - if matches!(v, Value::List(_)) && !matches!(column_type, InnerColumnType::Json { .. }) { + if matches!(v, Value::List(_)) && !matches!(inner, InnerColumnType::Json { .. }) { return Err(SettingsError::SchemaTypeValidationError { column: column_id.to_string(), expected_type: "Scalar".to_string(), @@ -23,7 +25,7 @@ fn _parse_value( }); } - match column_type { + match inner { InnerColumnType::Uuid {} => match v { Value::String(s) => { let value = s.parse::().map_err(|e| { @@ -427,9 +429,9 @@ async fn _validate_value( is_nullable: bool, ) -> Result { let v = match column_type { - ColumnType::Scalar { column_type } => { + ColumnType::Scalar { inner } => { // Special case: JSON columns can be any type - if matches!(v, Value::List(_)) && !matches!(column_type, InnerColumnType::Json { .. }) { + if matches!(v, Value::List(_)) && !matches!(inner, InnerColumnType::Json { .. }) { return Err(SettingsError::SchemaTypeValidationError { column: column_id.to_string(), expected_type: "Scalar".to_string(), @@ -437,7 +439,7 @@ async fn _validate_value( }); } - match column_type { + match inner { InnerColumnType::String { min_length, max_length, @@ -468,7 +470,7 @@ async fn _validate_value( } } - if !allowed_values.is_empty() && !allowed_values.contains(&s.as_str()) { + if !allowed_values.is_empty() && !allowed_values.contains(&s) { return Err(SettingsError::SchemaCheckValidationError { column: column_id.to_string(), check: "allowed_values".to_string(), @@ -651,27 +653,6 @@ async fn _validate_value( Ok(v) } -/// Returns the common filters for a given operation type -fn common_filters( - setting: &ConfigOption, - operation_type: OperationType, - base_state: &State, -) -> indexmap::IndexMap { - let common_filters_unparsed = setting - .common_filters - .get(&operation_type) - .unwrap_or(&setting.default_common_filters); - - let mut common_filters = indexmap::IndexMap::new(); - - for (key, value) in common_filters_unparsed.iter() { - let value = base_state.template_to_string(value); - common_filters.insert(key.to_string(), value); - } - - common_filters -} - /// Validate keys for basic sanity /// /// This *MUST* be called at the start of any operation to ensure that the keys are valid and safe @@ -699,178 +680,79 @@ pub async fn settings_view( author: serenity::all::UserId, fields: indexmap::IndexMap, // The filters to apply ) -> Result, SettingsError> { - let Some(operation_specific) = setting.operations.get(&OperationType::View) else { + if !setting.supported_operations.contains(&OperationType::View) { return Err(SettingsError::OperationNotSupported { operation: OperationType::View, }); - }; + } // WARNING: The ``validate_keys`` function call here should never be omitted, add back at once if you see this message without the function call validate_keys(setting, &fields)?; - let mut fields = fields; // Make fields mutable, consuming the input - - // Ensure limit is good - let mut use_limit = setting.max_return; - if let Some(Value::Integer(limit)) = fields.get("__limit") { - use_limit = std::cmp::min(*limit, use_limit); - } - fields.insert("__limit".to_string(), Value::Integer(use_limit)); + let Some(ref executor) = setting.executor.0 else { + return Ok(Vec::new()); + }; - let mut data_store = setting - .data_store - .create( - setting, + let states = executor + .view(HookContext { guild_id, author, data, - common_filters( - setting, - OperationType::View, - &super::state::State::new_with_special_variables(author, guild_id), - ), - ) - .await?; - - if let Some(Value::Boolean(true)) = fields.get("__count") { - // We only need to count the number of rows - fields.shift_remove("__limit"); - fields.shift_remove("__count"); - - let count = data_store.matching_entry_count(fields).await?; - - let count = count.try_into().map_err(|e| SettingsError::Generic { - message: format!("Count too large: {:?}", e), + }) + .await + .map_err(|e| SettingsError::Generic { + message: e.to_string(), src: "settings_view".to_string(), typ: "internal".to_string(), })?; - let mut state = super::state::State::new(); - - state - .state - .insert("count".to_string(), Value::Integer(count)); - - return Ok(vec![state]); - } - - let cols = setting - .columns - .iter() - .map(|c| c.id.to_string()) - .collect::>(); - - let states = data_store.fetch_all(&cols, fields).await?; - - if states.is_empty() { - return Ok(Vec::new()); - } - let mut values: Vec = Vec::new(); for mut state in states { // We know that the columns are in the same order as the row for col in setting.columns.iter() { - let mut val = state.state.swap_remove(col.id).unwrap_or(Value::None); + let mut val = state.state.swap_remove(&col.id).unwrap_or(Value::None); // Validate the value. returning the parsed value - val = _parse_value(val, &col.column_type, col.id)?; + val = _parse_value(val, &col.column_type, &col.id)?; // Reinsert state.state.insert(col.id.to_string(), val); } - // Run validators - - setting - .validator - .validate( - super::types::HookContext { - author, - guild_id, - operation_type: OperationType::View, - data_store: &mut *data_store, - data, - unchanged_fields: vec![], - }, - &mut state, - ) - .await?; - - // Get out the pkey and pkey_column data here as we need it for the rest of the update - let Some(pkey) = state.state.get(setting.primary_key) else { - return Err(SettingsError::MissingOrInvalidField { - field: setting.primary_key.to_string(), - src: "settings_update [pkey_let]".to_string(), - }); - }; - - // Apply columns_to_set in operation specific data if there are columns to set - if !operation_specific.columns_to_set.is_empty() { - let filters = indexmap::indexmap! { - setting.primary_key.to_string() => pkey.clone(), - }; - let mut update = indexmap::IndexMap::new(); - - for (col, value) in operation_specific.columns_to_set.iter() { - let value = state.template_to_string(value); - - // Add directly to state - state.state.insert(col.to_string(), value.clone()); - update.insert(col.to_string(), value); - } - - data_store.update_matching_entries(filters, update).await?; - } - // Remove ignored columns + secret columns now that the actions have been executed for col in setting.columns.iter() { if col.secret { - state.state.swap_remove(col.id); + state.state.swap_remove(&col.id); continue; // Skip secret columns in view. **this applies to view and update only as create is creating a new object** } - if state.bypass_ignore_for.contains(col.id) { + if state.bypass_ignore_for.contains(&col.id) { continue; } if col.ignored_for.contains(&OperationType::View) { - state.state.swap_remove(col.id); + state.state.swap_remove(&col.id); } } - setting - .post_action - .post_action( - super::types::HookContext { - author, - guild_id, - operation_type: OperationType::View, - data_store: &mut *data_store, - data, - unchanged_fields: vec![], - }, - &mut state, - ) - .await?; - values.push(state); } Ok(values) } -/// Settings API: Create implementation -pub async fn settings_create( +/// Settings API: Save implementation +pub async fn settings_save( setting: &ConfigOption, data: &SettingsData, guild_id: serenity::all::GuildId, author: serenity::all::UserId, fields: indexmap::IndexMap, ) -> Result { - let Some(operation_specific) = setting.operations.get(&OperationType::Create) else { + if !setting.supported_operations.contains(&OperationType::Save) { return Err(SettingsError::OperationNotSupported { - operation: OperationType::Create, + operation: OperationType::Save, }); }; @@ -885,13 +767,13 @@ pub async fn settings_create( // If the column is ignored for create, skip // If the column is a secret column, then ensure we set it to something random as this is a create operation let value = { - if column.ignored_for.contains(&OperationType::Create) { - _parse_value(Value::None, &column.column_type, column.id)? + if column.ignored_for.contains(&OperationType::Save) { + _parse_value(Value::None, &column.column_type, &column.id)? } else { // Get the value - let val = fields.swap_remove(column.id).unwrap_or(Value::None); + let val = fields.swap_remove(&column.id).unwrap_or(Value::None); - let parsed_value = _parse_value(val, &column.column_type, column.id)?; + let parsed_value = _parse_value(val, &column.column_type, &column.id)?; // Validate and parse the value _validate_value( @@ -899,7 +781,7 @@ pub async fn settings_create( guild_id, data, &column.column_type, - column.id, + &column.id, column.nullable, ) .await? @@ -907,78 +789,21 @@ pub async fn settings_create( }; // Insert the value into the state - state.state.insert( - column.id.to_string(), - match value { - Value::None => { - // Check for default - if let Some(default) = &column.default { - (default)(false) - } else { - value - } - } - _ => value, - }, - ); + state.state.insert(column.id.to_string(), value); } drop(fields); // Drop fields to avoid accidental use of user data #[allow(unused_variables)] let fields = (); // Reset fields to avoid accidental use of user data - // Start the transaction now that basic validation is done - let mut data_store = setting - .data_store - .create( - setting, - guild_id, - author, - data, - common_filters(setting, OperationType::Create, &state), - ) - .await?; - - data_store.start_transaction().await?; - - // Get all ids we currently have to check max_entries and uniqueness of the primary key in one shot - let ids = data_store - .fetch_all( - &[setting.primary_key.to_string()], - indexmap::IndexMap::new(), - ) - .await?; - - if let Some(max_entries) = setting.max_entries { - if ids.len() >= max_entries { - return Err(SettingsError::MaximumCountReached { - max: max_entries, - current: ids.len(), - }); - } - } - - for id in ids.iter() { - let id = id.state.get(setting.primary_key).unwrap_or(&Value::None); - // Check if the pkey is unique - if state.state.get(setting.primary_key) == Some(id) { - return Err(SettingsError::RowExists { - column_id: setting.primary_key.to_string(), - count: 1, - }); - } - } - - drop(ids); // Drop ids as it is no longer needed - - // Now execute all actions and handle null/unique/pkey checks + // Now execute all actions and handle null checks for column in setting.columns.iter() { // Checks should only happen if the column is not being intentionally ignored - if column.ignored_for.contains(&OperationType::Create) { + if column.ignored_for.contains(&OperationType::Save) { continue; } - let Some(value) = state.state.get(column.id) else { + let Some(value) = state.state.get(&column.id) else { return Err(SettingsError::Generic { message: format!( "Column `{}` not found in state despite just being parsed", @@ -996,358 +821,45 @@ pub async fn settings_create( src: "settings_create [null check]".to_string(), }); } - - // Handle cases of uniqueness - // - // In the case of create, we can do this directly within the column validation - if column.unique { - let count = data_store - .matching_entry_count(indexmap::indexmap! { - column.id.to_string() => value.clone() - }) - .await?; - - if count > 0 { - return Err(SettingsError::RowExists { - column_id: column.id.to_string(), - count: count.try_into().unwrap_or(i64::MAX), - }); - } - } } - // Run validator - setting - .validator - .validate( - super::types::HookContext { - author, - guild_id, - operation_type: OperationType::Create, - data_store: &mut *data_store, - data, - unchanged_fields: vec![], - }, - &mut state, - ) - .await?; - // Remove ignored columns now that the actions have been executed for col in setting.columns.iter() { - if state.bypass_ignore_for.contains(col.id) { + if state.bypass_ignore_for.contains(&col.id) { continue; } - if col.ignored_for.contains(&OperationType::Create) { - state.state.swap_remove(col.id); + if col.ignored_for.contains(&OperationType::Save) { + state.state.swap_remove(&col.id); } } - // Now insert all the columns_to_set into state - // As we have removed the ignored columns, we can just directly insert the columns_to_set into the state - for (column, value) in operation_specific.columns_to_set.iter() { - let value = state.template_to_string(value); - state.state.insert(column.to_string(), value); - } - // Create the row - let mut new_state = data_store.create_entry(state.get_public()).await?; - - // Insert any internal columns - for (key, value) in state - .state - .into_iter() - .filter(|(k, _)| k.starts_with(super::state::INTERNAL_KEY)) - { - new_state.state.insert(key, value); - } - - // Commit the transaction - data_store.commit().await?; - - // Execute post actions - setting - .post_action - .post_action( - super::types::HookContext { - author, - guild_id, - operation_type: OperationType::Create, - data_store: &mut *data_store, - data, - unchanged_fields: vec![], - }, - &mut new_state, - ) - .await?; - - Ok(new_state) -} - -/// Settings API: Update implementation -pub async fn settings_update( - setting: &ConfigOption, - data: &SettingsData, - guild_id: serenity::all::GuildId, - author: serenity::all::UserId, - fields: indexmap::IndexMap, -) -> Result { - let Some(operation_specific) = setting.operations.get(&OperationType::Update) else { - return Err(SettingsError::OperationNotSupported { - operation: OperationType::Update, - }); - }; - - // WARNING: The ``validate_keys`` function call here should never be omitted, add back at once if you see this message without the function call - validate_keys(setting, &fields)?; - - let mut fields = fields; // Make fields mutable, consuming the input - - // Ensure all columns exist in fields, note that we can ignore extra fields so this one single loop is enough - let mut state: State = State::new_with_special_variables(author, guild_id); - let mut unchanged_fields = indexmap::IndexSet::new(); - let mut pkey = None; - for column in setting.columns.iter() { - // If the column is ignored for update, skip - if column.ignored_for.contains(&OperationType::Update) && column.id != setting.primary_key { - if !column.secret { - unchanged_fields.insert(column.id.to_string()); // Ensure that ignored_for columns are still seen as unchanged but only if not secret - } - } else { - match fields.swap_remove(column.id) { - Some(val) => { - let parsed_value = _parse_value(val, &column.column_type, column.id)?; - - let parsed_value = _validate_value( - parsed_value, - guild_id, - data, - &column.column_type, - column.id, - column.nullable, - ) - .await?; - - if column.id == setting.primary_key { - pkey = Some((column, parsed_value.clone())); - } - - state.state.insert(column.id.to_string(), parsed_value); - } - None => { - if !column.secret { - unchanged_fields.insert(column.id.to_string()); // Don't retrieve the value if it's a secret column - } - } - } - } - } - - drop(fields); // Drop fields to avoid accidental use of user data - #[allow(unused_variables)] - let fields = (); // Reset fields to avoid accidental use of user data - - // Get out the pkey and pkey_column data here as we need it for the rest of the update - let Some((_pkey_column, pkey)) = pkey else { - return Err(SettingsError::MissingOrInvalidField { - field: setting.primary_key.to_string(), - src: "settings_update [pkey_let]".to_string(), + let Some(ref executor) = setting.executor.0 else { + return Err(SettingsError::Generic { + message: "No executor found".to_string(), + src: "settings_save".to_string(), + typ: "internal".to_string(), }); }; - // PKEY should already have passed the validation checks - if matches!(pkey, Value::None) { - return Err(SettingsError::MissingOrInvalidField { - field: setting.primary_key.to_string(), - src: "settings_update [pkey_none]".to_string(), - }); - } - - let mut data_store = setting - .data_store - .create( - setting, - guild_id, - author, - data, - common_filters(setting, OperationType::Update, &state), - ) - .await?; - - // Start the transaction now that basic validation is done - data_store.start_transaction().await?; - - // Now retrieve all the unchanged fields - if !unchanged_fields.is_empty() { - let mut data = data_store - .fetch_all( - &unchanged_fields - .iter() - .map(|f| f.to_string()) - .collect::>(), - indexmap::indexmap! { - setting.primary_key.to_string() => pkey.clone(), - }, - ) - .await?; - - if data.is_empty() { - return Err(SettingsError::RowDoesNotExist { - column_id: setting.primary_key.to_string(), - }); - } - - let unchanged_state = data.pop().unwrap(); // We know there is only one row - - for (k, v) in unchanged_state.state.into_iter() { - state.state.insert(k.to_string(), v); - } - } - - // Handle all the actual checks here, now that all validation and needed fetches are done - for column in setting.columns.iter() { - if column.ignored_for.contains(&OperationType::Update) { - continue; - } - - let Some(value) = state.state.get(column.id) else { - return Err(SettingsError::Generic { - message: format!( - "Column `{}` not found in state despite just being parsed", - column.id - ), - src: "settings_update [ext_checks]".to_string(), - typ: "internal".to_string(), - }); - }; - - // Nullability checks should only happen if the column is not being intentionally ignored - // Check if the column is nullable - if !column.nullable && matches!(value, Value::None) { - return Err(SettingsError::MissingOrInvalidField { - field: column.id.to_string(), - src: "settings_update [nullability check]".to_string(), - }); - } - - // Handle cases of uniqueness - // - // ** Difference from create: We can't treat unique and primary key the same as the unique check must take into account the existing row ** - if column.unique { - if unchanged_fields.contains(&column.id.to_string()) { - continue; // Skip uniqueness check if the field is unchanged - } - - let ids = data_store - .fetch_all( - &[setting.primary_key.to_string()], - indexmap::indexmap! { - column.id.to_string() => value.clone(), - }, - ) - .await?; - - let ids = ids - .into_iter() - .filter(|id| { - let id = id.state.get(column.id).unwrap_or(&Value::None); - id != &pkey - }) - .collect::>(); - - if !ids.is_empty() { - return Err(SettingsError::RowExists { - column_id: column.id.to_string(), - count: ids.len().try_into().unwrap_or(i64::MAX), - }); - } - } - - // Handle cases of primary key next - // ** This is unique to updates ** - if column.id == setting.primary_key { - let count = data_store - .matching_entry_count(indexmap::indexmap! { - column.id.to_string() => value.clone(), - }) - .await?; - - if count == 0 { - return Err(SettingsError::RowDoesNotExist { - column_id: column.id.to_string(), - }); - } - } - } - - // Run validator - setting - .validator - .validate( - super::types::HookContext { - author, + let new_state = executor + .save( + HookContext { guild_id, - operation_type: OperationType::Update, - data_store: &mut *data_store, - data, - unchanged_fields: unchanged_fields.iter().map(|f| f.to_string()).collect(), - }, - &mut state, - ) - .await?; - - // Remove ignored columns now that the actions have been executed - // - // Note that we cannot mutate state here - let mut columns_to_set = State::from_indexmap(state.get_public()); // Start with current public state - for col in setting.columns.iter() { - if state.bypass_ignore_for.contains(col.id) { - continue; - } - - if col.ignored_for.contains(&OperationType::Update) { - columns_to_set.state.swap_remove(col.id); - } - } - - // Now insert all the columns_to_set into state - // As we have removed the ignored columns, we can just directly insert the columns_to_set into the state - for (column, value) in operation_specific.columns_to_set.iter() { - let value = state.template_to_string(value); - state.state.insert(column.to_string(), value.clone()); // Ensure its in returned state - columns_to_set.state.insert(column.to_string(), value); // And in the columns to set - } - - // Create the row - data_store - .update_matching_entries( - indexmap::indexmap! { - setting.primary_key.to_string() => pkey.clone(), - }, - columns_to_set.state, - ) - .await?; - - // Commit the transaction - data_store.commit().await?; - - // Execute post actions - setting - .post_action - .post_action( - super::types::HookContext { author, - guild_id, - operation_type: OperationType::Update, - data_store: &mut *data_store, data, - unchanged_fields: unchanged_fields.iter().map(|f| f.to_string()).collect(), }, &mut state, ) - .await?; + .await + .map_err(|e| SettingsError::Generic { + message: e.to_string(), + src: "settings_save".to_string(), + typ: "internal".to_string(), + })?; - Ok(state) + Ok(new_state) } /// Settings API: Delete implementation @@ -1359,13 +871,14 @@ pub async fn settings_delete( author: serenity::all::UserId, pkey: Value, ) -> Result { - let Some(_operation_specific) = setting.operations.get(&OperationType::Delete) else { + if !setting + .supported_operations + .contains(&OperationType::Delete) + { return Err(SettingsError::OperationNotSupported { operation: OperationType::Delete, }); - }; - - let state = State::new_with_special_variables(author, guild_id); + } let Some(pkey_column) = setting.columns.iter().find(|c| c.id == setting.primary_key) else { return Err(SettingsError::Generic { @@ -1375,87 +888,32 @@ pub async fn settings_delete( }); }; - let pkey = _parse_value(pkey, &pkey_column.column_type, setting.primary_key)?; + let pkey = _parse_value(pkey, &pkey_column.column_type, &setting.primary_key)?; - let mut data_store = setting - .data_store - .create( - setting, - guild_id, - author, - data, - common_filters(setting, OperationType::Delete, &state), - ) - .await?; - - // Start the transaction now that basic validation is done - data_store.start_transaction().await?; - - // Fetch entire row to execute actions on before deleting - let cols = setting - .columns - .iter() - .map(|c| c.id.to_string()) - .collect::>(); - - let mut state = data_store - .fetch_all( - &cols, - indexmap::indexmap! { - setting.primary_key.to_string() => pkey.clone(), - }, - ) - .await?; - - if state.is_empty() { - return Err(SettingsError::RowDoesNotExist { - column_id: setting.primary_key.to_string(), + // Create the row + let Some(ref executor) = setting.executor.0 else { + return Err(SettingsError::Generic { + message: "No executor found".to_string(), + src: "settings_save".to_string(), + typ: "internal".to_string(), }); - } - - let mut state = state.pop().unwrap(); // We know there is only one row + }; - // Run validator - setting - .validator - .validate( - super::types::HookContext { - author, + let state = executor + .delete( + HookContext { guild_id, - operation_type: OperationType::Delete, - data_store: &mut *data_store, - data, - unchanged_fields: vec![], - }, - &mut state, - ) - .await?; - - // Now delete the entire row, the ignored_for does not matter here as we are deleting the entire row - data_store - .delete_matching_entries(indexmap::indexmap! { - setting.primary_key.to_string() => pkey.clone(), - }) - .await?; - - // Commit the transaction - data_store.commit().await?; - - // Execute post actions - setting - .post_action - .post_action( - super::types::HookContext { author, - guild_id, - operation_type: OperationType::Delete, - data_store: &mut *data_store, data, - unchanged_fields: vec![], }, - &mut state, + pkey, ) - .await?; + .await + .map_err(|e| SettingsError::Generic { + message: e.to_string(), + src: "settings_delete".to_string(), + typ: "internal".to_string(), + })?; Ok(state) } diff --git a/core/rust.module_settings/src/common_columns.rs b/core/rust.module_settings/src/common_columns.rs index 66179092..036ff983 100644 --- a/core/rust.module_settings/src/common_columns.rs +++ b/core/rust.module_settings/src/common_columns.rs @@ -5,14 +5,12 @@ use super::types::{ /// Standard created_at column pub fn created_at() -> Column { Column { - id: "created_at", - name: "Created At", - description: "The time the record was created.", + id: "created_at".to_string(), + name: "Created At".to_string(), + description: "The time the record was created.".to_string(), column_type: ColumnType::new_scalar(InnerColumnType::TimestampTz {}), nullable: false, - default: None, - unique: false, - ignored_for: vec![OperationType::Create, OperationType::Update], + ignored_for: vec![OperationType::Save], secret: false, suggestions: ColumnSuggestion::None {}, } @@ -21,20 +19,18 @@ pub fn created_at() -> Column { /// Standard created_by column pub fn created_by() -> Column { Column { - id: "created_by", - name: "Created By", - description: "The user who created the record.", + id: "created_by".to_string(), + name: "Created By".to_string(), + description: "The user who created the record.".to_string(), column_type: ColumnType::new_scalar(InnerColumnType::String { min_length: None, max_length: None, allowed_values: vec![], kind: InnerColumnTypeStringKind::User, }), - default: None, - ignored_for: vec![OperationType::Create, OperationType::Update], + ignored_for: vec![OperationType::Save], secret: false, nullable: false, - unique: false, suggestions: ColumnSuggestion::None {}, } } @@ -42,15 +38,13 @@ pub fn created_by() -> Column { /// Standard last_updated_at column pub fn last_updated_at() -> Column { Column { - id: "last_updated_at", - name: "Last Updated At", - description: "The time the record was last updated.", + id: "last_updated_at".to_string(), + name: "Last Updated At".to_string(), + description: "The time the record was last updated.".to_string(), column_type: ColumnType::new_scalar(InnerColumnType::TimestampTz {}), - ignored_for: vec![OperationType::Create, OperationType::Update], + ignored_for: vec![OperationType::Save], secret: false, nullable: false, - default: None, - unique: false, suggestions: ColumnSuggestion::None {}, } } @@ -58,29 +52,27 @@ pub fn last_updated_at() -> Column { /// Standard last_updated_by column pub fn last_updated_by() -> Column { Column { - id: "last_updated_by", - name: "Last Updated By", - description: "The user who last updated the record.", + id: "last_updated_by".to_string(), + name: "Last Updated By".to_string(), + description: "The user who last updated the record.".to_string(), column_type: ColumnType::new_scalar(InnerColumnType::String { min_length: None, max_length: None, allowed_values: vec![], kind: InnerColumnTypeStringKind::User, }), - ignored_for: vec![OperationType::Create, OperationType::Update], + ignored_for: vec![OperationType::Save], secret: false, nullable: false, - default: None, - unique: false, suggestions: ColumnSuggestion::None {}, } } pub fn guild_id(id: &'static str, name: &'static str, description: &'static str) -> Column { Column { - id, - name, - description, + id: id.to_string(), + name: name.to_string(), + description: description.to_string(), column_type: ColumnType::new_scalar(InnerColumnType::String { min_length: None, max_length: None, @@ -88,10 +80,8 @@ pub fn guild_id(id: &'static str, name: &'static str, description: &'static str) kind: InnerColumnTypeStringKind::Normal, }), nullable: false, - default: None, - unique: false, suggestions: ColumnSuggestion::None {}, - ignored_for: vec![OperationType::Create, OperationType::Update], + ignored_for: vec![OperationType::Save], secret: false, } } diff --git a/core/rust.module_settings/src/data_stores.rs b/core/rust.module_settings/src/data_stores.rs deleted file mode 100644 index 905f650a..00000000 --- a/core/rust.module_settings/src/data_stores.rs +++ /dev/null @@ -1,740 +0,0 @@ -use super::state::State; -use super::types::{ - Column, ColumnType, ConfigOption, CreateDataStore, DataStore, InnerColumnType, SettingsData, - SettingsError, -}; -use async_trait::async_trait; -use splashcore_rs::{utils::sql_utils, value::Value}; -use sqlx::{Execute, Row}; -use std::sync::Arc; - -/// Simple macro to combine two indexmaps into one -macro_rules! combine_indexmaps { - ($map1:expr, $map2:expr) => {{ - let mut map = $map1; - map.extend($map2); - map - }}; -} - -pub struct PostgresDataStore {} - -impl PostgresDataStore { - /// Creates a new PostgresDataStoreImpl. This is exposed as it is useful for making wrapper data stores - pub async fn create_impl( - &self, - setting: &ConfigOption, - guild_id: serenity::all::GuildId, - author: serenity::all::UserId, - data: &SettingsData, - common_filters: indexmap::IndexMap, - ) -> Result { - Ok(PostgresDataStoreImpl { - tx: None, - setting_table: setting.table, - setting_primary_key: setting.primary_key, - author, - guild_id, - columns: setting.columns.clone(), - valid_columns: setting.columns.iter().map(|c| c.id.to_string()).collect(), - pool: data.pool.clone(), - common_filters, - }) - } -} - -#[async_trait] -impl CreateDataStore for PostgresDataStore { - async fn create( - &self, - setting: &ConfigOption, - guild_id: serenity::all::GuildId, - author: serenity::all::UserId, - data: &SettingsData, - common_filters: indexmap::IndexMap, - ) -> Result, SettingsError> { - Ok(Box::new( - self.create_impl(setting, guild_id, author, data, common_filters) - .await?, - )) - } -} - -pub struct PostgresDataStoreImpl { - // Args needed for queries - pub pool: sqlx::PgPool, - pub setting_table: &'static str, - pub setting_primary_key: &'static str, - pub author: serenity::all::UserId, - pub guild_id: serenity::all::GuildId, - pub columns: Arc>, - pub valid_columns: std::collections::HashSet, // Derived from columns - pub common_filters: indexmap::IndexMap, - - // Transaction (if ongoing) - pub tx: Option>, -} - -impl PostgresDataStoreImpl { - pub fn from_data_store(d: &mut dyn DataStore) -> Result<&mut Self, SettingsError> { - d.as_any() - .downcast_mut::() - .ok_or(SettingsError::Generic { - message: "Failed to downcast to PostgresDataStoreImpl".to_string(), - src: "PostgresDataStoreImpl::from_data_store".to_string(), - typ: "internal".to_string(), - }) - } - - /// Binds a value to a query - /// - /// Note that Maps are binded as JSONs - /// - /// `default_column_type` - The (default) column type to use if the value is None. This should be the column_type - fn _query_bind_value<'a>( - query: sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments>, - value: Value, - default_column_type: &ColumnType, - ) -> sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments> { - match value { - Value::Uuid(value) => query.bind(value), - Value::String(value) => query.bind(value), - Value::Timestamp(value) => query.bind(value), - Value::TimestampTz(value) => query.bind(value), - Value::Interval(value) => query.bind(value), - Value::Integer(value) => query.bind(value), - Value::Float(value) => query.bind(value), - Value::Boolean(value) => query.bind(value), - Value::Json(value) => query.bind(value), - Value::List(values) => { - // Get the type of the first element - let first = values.first(); - - if let Some(first) = first { - // This is hacky and long but sqlx doesn't support binding lists - // - // Loop over all values to make a Vec then bind that - match first { - Value::Uuid(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Uuid(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::String(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::String(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::Timestamp(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Timestamp(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::TimestampTz(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::TimestampTz(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::Interval(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Interval(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::Integer(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Integer(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::Float(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Float(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - Value::Boolean(_) => { - let mut vec = Vec::new(); - - for value in values { - if let Value::Boolean(value) = value { - vec.push(value); - } - } - - query.bind(vec) - } - // In all other cases (list/map) - Value::Map(_) => { - let mut vec = Vec::new(); - - for value in values { - vec.push(value.to_json()); - } - - query.bind(vec) - } - Value::List(_) => { - let mut vec = Vec::new(); - - for value in values { - vec.push(value.to_json()); - } - - query.bind(vec) - } - Value::Json(_) => { - let mut vec = Vec::new(); - - for value in values { - vec.push(value.to_json()); - } - - query.bind(vec) - } - Value::None => { - let vec: Vec = Vec::new(); - query.bind(vec) - } - } - } else { - let vec: Vec = Vec::new(); - query.bind(vec) - } - } - Value::Map(_) => query.bind(value.to_json()), - Value::None => match default_column_type { - ColumnType::Scalar { - column_type: column_type_hint, - } => match column_type_hint { - InnerColumnType::Uuid {} => query.bind(None::), - InnerColumnType::String { .. } => query.bind(None::), - InnerColumnType::Timestamp {} => query.bind(None::), - InnerColumnType::TimestampTz {} => { - query.bind(None::>) - } - InnerColumnType::Interval {} => query.bind(None::), - InnerColumnType::Integer {} => query.bind(None::), - InnerColumnType::Float {} => query.bind(None::), - InnerColumnType::BitFlag { .. } => query.bind(None::), - InnerColumnType::Boolean {} => query.bind(None::), - InnerColumnType::Json { .. } => query.bind(None::), - }, - ColumnType::Array { - inner: column_type_hint, - } => match column_type_hint { - InnerColumnType::Uuid {} => query.bind(None::>), - InnerColumnType::String { .. } => query.bind(None::>), - InnerColumnType::Timestamp {} => query.bind(None::>), - InnerColumnType::TimestampTz {} => { - query.bind(None::>>) - } - InnerColumnType::Interval {} => query.bind(None::>), - InnerColumnType::Integer {} => query.bind(None::>), - InnerColumnType::Float {} => query.bind(None::>), - InnerColumnType::BitFlag { .. } => query.bind(None::>), - InnerColumnType::Boolean {} => query.bind(None::>), - InnerColumnType::Json { .. } => query.bind(None::>), - }, - }, - } - } - - /// Binds filters to a query - /// - /// If bind_nulls is true, then entries with Value::None are also binded. This should be disabled on filters and enabled on entries - fn bind_map<'a>( - query: sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments>, - map: indexmap::IndexMap, - bind_nulls: bool, - columns: &[Column], - ) -> Result, SettingsError> - { - let mut query = query; - - let mut spec_limit: Option = None; - let mut spec_offset: Option = None; - for (field_name, value) in map { - if field_name == "__limit" { - if let Value::Integer(value) = value { - if value < 1 { - return Err(SettingsError::Generic { - message: "__limit must be greater than 0".to_string(), - src: "PostgresDataStore#bind_map".to_string(), - typ: "internal".to_string(), - }); - } - spec_limit = Some(value); - } - continue; - } else if field_name == "__offset" { - if let Value::Integer(value) = value { - if value < 0 { - return Err(SettingsError::Generic { - message: "__offset must be greater than or equal to 0".to_string(), - src: "PostgresDataStore#bind_map".to_string(), - typ: "internal".to_string(), - }); - } - spec_offset = Some(value); - } - continue; - } - - // If None, we omit the value from binding - if !bind_nulls && matches!(value, Value::None) { - continue; - } - - let column = - columns - .iter() - .find(|c| c.id == field_name) - .ok_or(SettingsError::Generic { - message: format!("Column {} not found", field_name), - src: "settings_view [fetch_all]".to_string(), - typ: "internal".to_string(), - })?; - - query = Self::_query_bind_value(query, value, &column.column_type); - } - - // Add limit and offset last - if let Some(limit) = spec_limit { - query = query.bind(limit); - } - - if let Some(offset) = spec_offset { - query = query.bind(offset); - } - - Ok(query) - } - - /// Helper method to either perform a perform a query using either the transaction or the pool - async fn execute_query<'a>( - &mut self, - query: sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments>, - ) -> Result { - // Get the transaction connection or acquire one from pool if not in a transaction - let conn = if self.tx.is_some() { - self.tx.as_deref_mut().unwrap() - } else { - &mut *self - .pool - .acquire() - .await - .map_err(|e| SettingsError::Generic { - message: format!("Failed to get connection: {:?}", e), - src: "PostgresDataStore::execute_query [query_execute]".to_string(), - typ: "internal".to_string(), - })? - }; - - query - .execute(&mut *conn) - .await - .map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::execute_query [query_execute]".to_string(), - typ: "internal".to_string(), - }) - } - - /// Helper method to either perform a perform a query using either the transaction or the pool - async fn fetchone_query<'a>( - &mut self, - query: sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments>, - ) -> Result { - let query_sql = query.sql(); - - // Get the transaction connection or acquire one from pool if not in a transaction - let conn = if self.tx.is_some() { - self.tx.as_deref_mut().unwrap() - } else { - &mut *self - .pool - .acquire() - .await - .map_err(|e| SettingsError::Generic { - message: format!("Failed to get connection: {:?}", e), - src: "PostgresDataStore::fetchone_query".to_string(), - typ: "internal".to_string(), - })? - }; - - query - .fetch_one(&mut *conn) - .await - .map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: format!( - "PostgresDataStore::fetchone_query [query_execute]: {}", - query_sql - ), - typ: "internal".to_string(), - }) - } - - /// Helper method to either perform a perform a query using either the transaction or the pool - async fn fetchall_query<'a>( - &mut self, - query: sqlx::query::Query<'a, sqlx::Postgres, sqlx::postgres::PgArguments>, - ) -> Result, SettingsError> { - let query_sql = query.sql(); - - let conn = if self.tx.is_some() { - self.tx.as_deref_mut().unwrap() - } else { - &mut *self - .pool - .acquire() - .await - .map_err(|e| SettingsError::Generic { - message: format!("Failed to get connection: {:?}", e), - src: "PostgresDataStore::fetchall_query".to_string(), - typ: "internal".to_string(), - })? - }; - - query - .fetch_all(&mut *conn) - .await - .map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: format!( - "PostgresDataStore::fetchall_query [query_execute]: {}", - query_sql - ), - typ: "internal".to_string(), - }) - } - - fn filter_fields( - fields: &[String], - valid_columns: &std::collections::HashSet, - ) -> Vec { - let mut new_fields = Vec::new(); - - for f in fields { - if valid_columns.contains(f) { - new_fields.push(f.to_string()); - } - } - - new_fields - } -} - -#[async_trait] -impl DataStore for PostgresDataStoreImpl { - fn as_any(&mut self) -> &mut dyn std::any::Any { - self - } - - async fn start_transaction(&mut self) -> Result<(), SettingsError> { - let tx: sqlx::Transaction<'_, sqlx::Postgres> = - self.pool - .begin() - .await - .map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::start_transaction [pool.begin]".to_string(), - typ: "internal".to_string(), - })?; - - self.tx = Some(tx); - - Ok(()) - } - - async fn commit(&mut self) -> Result<(), SettingsError> { - if let Some(tx) = self.tx.take() { - tx.commit().await.map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::commit [tx.commit]".to_string(), - typ: "internal".to_string(), - })?; - } - - Ok(()) - } - - async fn columns(&mut self) -> Result, SettingsError> { - // Get columns from database - let query = sqlx::query("SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position") - .bind(self.setting_table); - - let rows = self.fetchall_query(query).await?; - - let mut columns = Vec::new(); - - for row in rows { - let column_name: String = - row.try_get("column_name") - .map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::columns [row try_get]".to_string(), - typ: "internal".to_string(), - })?; - - columns.push(column_name); - } - - Ok(columns) - } - - async fn fetch_all( - &mut self, - fields: &[String], - filters: indexmap::IndexMap, - ) -> Result, SettingsError> { - let filters = combine_indexmaps!(filters, self.common_filters.clone()); - - let sql_stmt = format!( - "SELECT {} FROM {} WHERE {}", - PostgresDataStoreImpl::filter_fields(fields, &self.valid_columns).join(", "), - self.setting_table, - sql_utils::create_where_clause(&self.valid_columns, &filters, 0).map_err(|e| { - SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::fetch_all [create_where_clause]".to_string(), - typ: "internal".to_string(), - } - })? - ); - - let mut query = sqlx::query(sql_stmt.as_str()); - - if !filters.is_empty() { - query = Self::bind_map(query, filters, false, &self.columns)?; - } - - // Execute the query and process it to a Vec - let rows = self.fetchall_query(query).await?; - - let mut values: Vec = Vec::new(); - for row in rows { - let mut state = State::new_with_special_variables(self.author, self.guild_id); // Ensure special vars are in the state - - for (i, col) in fields.iter().enumerate() { - let val = Value::from_sqlx(&row, i).map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::rows_to_states [Value::from_sqlx]".to_string(), - typ: "internal".to_string(), - })?; - - state.state.insert(col.to_string(), val); - } - - values.push(state); - } - - Ok(values) - } - - async fn matching_entry_count( - &mut self, - filters: indexmap::IndexMap, - ) -> Result { - let filters = combine_indexmaps!(filters, self.common_filters.clone()); - - let sql_stmt = format!( - "SELECT COUNT(*) FROM {} WHERE {}", - self.setting_table, - sql_utils::create_where_clause(&self.valid_columns, &filters, 0).map_err(|e| { - SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::matching_entry_count [create_where_clause]" - .to_string(), - typ: "internal".to_string(), - } - })? - ); - - let mut query = sqlx::query(sql_stmt.as_str()); - - if !filters.is_empty() { - query = Self::bind_map(query, filters, false, &self.columns)?; - } - - // Execute the query - let row = self.fetchone_query(query).await?; - - let count: i64 = row.try_get(0).map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "PostgresDataStore::matching_entry_count [row try_get]".to_string(), - typ: "internal".to_string(), - })?; - - Ok(count as usize) - } - - /// Creates a new entry given a set of columns to set returning the newly created entry - async fn create_entry( - &mut self, - entry: indexmap::IndexMap, - ) -> Result { - let entry = combine_indexmaps!(entry, self.common_filters.clone()); - - // Create the row - let (col_params, n_params) = - sql_utils::create_col_and_n_params(&self.valid_columns, &entry, 0).map_err(|e| { - SettingsError::Generic { - message: e.to_string(), - src: "settings_create [create_col_and_n_params]".to_string(), - typ: "internal".to_string(), - } - })?; - - // Execute the SQL statement - let sql_stmt = format!( - "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", - self.setting_table, col_params, n_params, self.setting_primary_key - ); - - let mut query = sqlx::query(sql_stmt.as_str()); - - // Bind the sql query arguments - let mut state = State::from_indexmap(entry.clone()); - - query = Self::bind_map(query, entry, true, &self.columns)?; - - // Execute the query - let pkey_row = self.fetchone_query(query).await?; - - // Save pkey to state - state.state.insert( - self.setting_primary_key.to_string(), - Value::from_sqlx(&pkey_row, 0).map_err(|e| SettingsError::Generic { - message: e.to_string(), - src: "settings_create [Value::from_sqlx]".to_string(), - typ: "internal".to_string(), - })?, - ); - - Ok(state) - } - - /// Updates an entry given a set of columns to set and a set of filters returning the updated entry - /// - /// Note that only the fields to be updated should be passed to this function - async fn update_matching_entries( - &mut self, - filters: indexmap::IndexMap, - entry: indexmap::IndexMap, - ) -> Result<(), SettingsError> { - let filters = combine_indexmaps!(filters, self.common_filters.clone()); - - // Create the SQL statement - let sql_stmt = format!( - "UPDATE {} SET {} WHERE {}", - self.setting_table, - sql_utils::create_update_set_clause(&self.valid_columns, &entry, 0).map_err(|e| { - SettingsError::Generic { - message: e.to_string(), - src: "settings_update [create_update_set_clause]".to_string(), - typ: "internal".to_string(), - } - })?, - sql_utils::create_where_clause(&self.valid_columns, &filters, entry.len()).map_err( - |e| { - SettingsError::Generic { - message: e.to_string(), - src: "settings_update [create_where_clause]".to_string(), - typ: "internal".to_string(), - } - } - )? - ); - - let mut query = sqlx::query(sql_stmt.as_str()); - - query = Self::bind_map(query, entry, true, &self.columns)?; // Bind the entry - query = Self::bind_map(query, filters, false, &self.columns)?; // Bind the filters - - // Execute the query - self.execute_query(query).await?; - - Ok(()) - } - - /// Deletes entries given a set of filters - /// - /// Returns all deleted rows - async fn delete_matching_entries( - &mut self, - filters: indexmap::IndexMap, - ) -> Result<(), SettingsError> { - let filters = combine_indexmaps!(filters, self.common_filters.clone()); - - // Create the SQL statement - let sql_stmt = format!( - "DELETE FROM {} WHERE {}", - self.setting_table, - sql_utils::create_where_clause(&self.valid_columns, &filters, 0).map_err(|e| { - SettingsError::Generic { - message: e.to_string(), - src: "settings_delete [create_where_clause]".to_string(), - typ: "internal".to_string(), - } - })? - ); - - let mut query = sqlx::query(sql_stmt.as_str()); - - if !filters.is_empty() { - query = Self::bind_map(query, filters, false, &self.columns)?; - } - - // Execute the query - let res = self.execute_query(query).await?; - - if res.rows_affected() == 0 { - return Err(SettingsError::RowDoesNotExist { - column_id: self.setting_primary_key.to_string(), - }); - } - - Ok(()) - } -} diff --git a/core/rust.module_settings/src/lib.rs b/core/rust.module_settings/src/lib.rs index 99c0004c..39987147 100644 --- a/core/rust.module_settings/src/lib.rs +++ b/core/rust.module_settings/src/lib.rs @@ -1,6 +1,4 @@ -pub mod canonical_types; pub mod cfg; pub mod common_columns; -pub mod data_stores; pub mod state; pub mod types; diff --git a/core/rust.module_settings/src/types.rs b/core/rust.module_settings/src/types.rs index dfcf5768..db168078 100644 --- a/core/rust.module_settings/src/types.rs +++ b/core/rust.module_settings/src/types.rs @@ -1,15 +1,10 @@ use async_trait::async_trait; use std::sync::Arc; -pub struct SettingsData { - pub pool: sqlx::PgPool, - pub reqwest: reqwest::Client, - pub object_store: Arc, - pub cache_http: botox::cache::CacheHttpImpl, - pub serenity_context: serenity::all::Context, -} +pub type Error = Box; // This is constant and should be copy pasted -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] pub enum SettingsError { /// Operation not supported OperationNotSupported { @@ -56,67 +51,27 @@ pub enum SettingsError { }, } -impl std::fmt::Display for SettingsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SettingsError::Generic { message, src, typ } => { - write!(f, "```{}```\n**Source:** `{}`\n**Error Type:** `{}`", message, src, typ) - } - SettingsError::OperationNotSupported { operation } => { - write!(f, "Operation `{}` is not supported", operation) - } - SettingsError::SchemaTypeValidationError { - column, - expected_type, - got_type, - } => write!( - f, - "Column `{}` expected type `{}`, got type `{}`", - column, expected_type, got_type - ), - SettingsError::SchemaNullValueValidationError { column } => { - write!(f, "Column `{}` is not nullable, yet value is null", column) - } - SettingsError::SchemaCheckValidationError { - column, - check, - error, - accepted_range, - } => { - write!( - f, - "Column `{}` failed check `{}`, accepted range: `{}`, error: `{}`", - column, check, accepted_range, error - ) - } - SettingsError::MissingOrInvalidField { field, src } => write!(f, "Missing (or invalid) field `{}` with src: `{}`", field, src), - SettingsError::RowExists { column_id, count } => write!( - f, - "A row with the same column `{}` already exists. Count: `{}`", - column_id, count - ), - SettingsError::RowDoesNotExist { column_id } => { - write!(f, "A row with the same column `{}` does not exist", column_id) - } - SettingsError::MaximumCountReached { max, current } => write!( - f, - "The maximum number of entities this server may have (`{}`) has been reached. This server currently has `{}`.", - max, current - ), - } - } +pub struct SettingsData { + pub pool: sqlx::PgPool, + pub reqwest: reqwest::Client, + pub object_store: Arc, + pub cache_http: botox::cache::CacheHttpImpl, + pub serenity_context: serenity::all::Context, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[allow(dead_code)] +#[serde(tag = "type")] pub enum ColumnType { /// A single valued column (scalar) Scalar { + #[serde(flatten)] /// The value type - column_type: InnerColumnType, + inner: InnerColumnType, }, /// An array column Array { + #[serde(flatten)] /// The inner type of the array inner: InnerColumnType, }, @@ -136,7 +91,7 @@ impl ColumnType { } pub fn new_scalar(inner: InnerColumnType) -> Self { - ColumnType::Scalar { column_type: inner } + ColumnType::Scalar { inner } } pub fn new_array(inner: InnerColumnType) -> Self { @@ -144,17 +99,9 @@ impl ColumnType { } } -impl std::fmt::Display for ColumnType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ColumnType::Scalar { column_type } => write!(f, "{}", column_type), - ColumnType::Array { inner } => write!(f, "Array<{}>", inner), - } - } -} - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[allow(dead_code)] +#[serde(tag = "type")] pub enum InnerColumnTypeStringKind { /// Normal string Normal, @@ -164,12 +111,9 @@ pub enum InnerColumnTypeStringKind { default_length: usize, }, /// A textarea - Textarea { ctx: &'static str }, + Textarea { ctx: String }, /// A reference to a template by name - TemplateRef { - kind: &'static str, - ctx: &'static str, - }, + TemplateRef { kind: String, ctx: String }, /// A kittycat permission KittycatPermission, /// User @@ -184,35 +128,15 @@ pub enum InnerColumnTypeStringKind { Modifier, } -impl std::fmt::Display for InnerColumnTypeStringKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InnerColumnTypeStringKind::Normal => write!(f, "Normal"), - InnerColumnTypeStringKind::Token { default_length } => { - write!(f, "Token (default_length: {})", default_length) - } - InnerColumnTypeStringKind::Textarea { ctx } => write!(f, "Textarea (ctx: {})", ctx), - InnerColumnTypeStringKind::TemplateRef { kind, ctx } => { - write!(f, "TemplateRef (kind: {}, ctx: {})", kind, ctx) - } - InnerColumnTypeStringKind::KittycatPermission => write!(f, "KittycatPermission"), - InnerColumnTypeStringKind::User => write!(f, "User"), - InnerColumnTypeStringKind::Role => write!(f, "Role"), - InnerColumnTypeStringKind::Emoji => write!(f, "Emoji"), - InnerColumnTypeStringKind::Message => write!(f, "Message"), - InnerColumnTypeStringKind::Modifier => write!(f, "Modifier"), - } - } -} - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[allow(dead_code)] +#[serde(tag = "type")] pub enum InnerColumnType { Uuid {}, String { min_length: Option, max_length: Option, - allowed_values: Vec<&'static str>, // If empty, all values are allowed + allowed_values: Vec, // If empty, all values are allowed kind: InnerColumnTypeStringKind, }, Timestamp {}, @@ -230,76 +154,24 @@ pub enum InnerColumnType { }, } -impl std::fmt::Display for InnerColumnType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InnerColumnType::Uuid {} => write!(f, "Uuid"), - InnerColumnType::String { - min_length, - max_length, - allowed_values, - kind, - } => { - write!(f, "String {}", kind)?; - if let Some(min) = min_length { - write!(f, " (min length: {})", min)?; - } - if let Some(max) = max_length { - write!(f, " (max length: {})", max)?; - } - if !allowed_values.is_empty() { - write!(f, " (allowed values: {:?})", allowed_values)?; - } - Ok(()) - } - InnerColumnType::Timestamp {} => write!(f, "Timestamp"), - InnerColumnType::TimestampTz {} => write!(f, "TimestampTz"), - InnerColumnType::Interval {} => write!(f, "Interval"), - InnerColumnType::Integer {} => write!(f, "Integer"), - InnerColumnType::Float {} => write!(f, "Float"), - InnerColumnType::BitFlag { values } => { - write!(f, "BitFlag (values: ")?; - for (i, (key, value)) in values.iter().enumerate() { - if i != 0 { - write!(f, ", ")?; - } - write!(f, "{}: {}", key, value)?; - } - write!(f, ")") - } - InnerColumnType::Boolean {} => write!(f, "Boolean"), - InnerColumnType::Json { max_bytes } => write!(f, "Json (max bytes: {:?})", max_bytes), - } - } -} - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] pub enum ColumnSuggestion { - Static { - suggestions: Vec<&'static str>, - }, - /// A reference to another setting - /// - /// The primary key of the referred setting is used as the value - SettingsReference { - /// The module of the referenced setting - module: &'static str, - /// The setting of the referenced setting - setting: &'static str, - }, + Static { suggestions: Vec }, None {}, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] pub struct Column { /// The ID of the column on the database - pub id: &'static str, + pub id: String, /// The friendly name of the column - pub name: &'static str, + pub name: String, /// The description of the column - pub description: &'static str, + pub description: String, /// The type of the column pub column_type: ColumnType, @@ -309,40 +181,16 @@ pub struct Column { /// Note that the point where nullability is checked may vary but will occur after pre_checks are executed pub nullable: bool, - /// The default value of the column - pub default: Option splashcore_rs::value::Value>, - /// Suggestions to display pub suggestions: ColumnSuggestion, - /// Whether or not the column is unique - /// - /// Note that the point where uniqueness is checked may vary but will occur after pre_checks are executed - pub unique: bool, + /// A secret field that is not shown to the user + pub secret: bool, /// For which operations should the field be ignored for (essentially, read only) /// - /// Note that checks for this column will still be applied (use an empty array in pre_checks to disable checks) - /// - /// Semantics: - /// - /// View => The column is removed from the list of columns sent to the consumer. The value is set to its current value when executing the actions - /// - /// Create => All column checks other than actions are ignored. The value itself may or may not be set. The key itself is set to None in state - /// - /// Update => All column checks other than actions are ignored. The value itself will be set to its current (on-database) value [an unchanged field]. - /// - /// Delete => No real effect. The column will still be set in state for Delete operations for actions to consume them. + /// Semantics are defined by the Executor pub ignored_for: Vec, - - /// Whether or not the column is a secret - /// - /// Note that secret columns are not present in view or update actions unless explicitly provided by the user. ignored_for rules continue to apply. - /// - /// The exact semantics of a secret column depend on column type (a String of kind token will lead to autogeneration of a token for example) - /// - /// Due to secret still following ignore_for rules and internal implementation reasons, tokens etc. will not be autogenerated if the column has ignored_for set. In this case, a native action must be used - pub secret: bool, } impl PartialEq for Column { @@ -351,120 +199,42 @@ impl PartialEq for Column { } } -#[derive(Debug, Clone, PartialEq)] -pub struct OperationSpecific { - /// Any columns to set. For example, a last_updated column should be set on update - /// - /// Variables: - /// - {now} => the current timestamp - /// - /// Format: {column_name} => {value} - /// - /// Note: updating columns outside of the table itself - /// - /// In Create/Update, these columns are directly included in the create/update itself - pub columns_to_set: indexmap::IndexMap<&'static str, &'static str>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, serde::Serialize, serde::Deserialize)] #[allow(dead_code)] pub enum OperationType { View, - Create, - Update, + Save, Delete, } -impl OperationType { - pub fn corresponding_command_suffix(&self) -> &'static str { - match self { - OperationType::View => "view", - OperationType::Create => "create", - OperationType::Update => "update", - OperationType::Delete => "delete", - } - } -} - -impl std::fmt::Display for OperationType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - OperationType::View => write!(f, "View"), - OperationType::Create => write!(f, "Create"), - OperationType::Update => write!(f, "Update"), - OperationType::Delete => write!(f, "Delete"), - } - } -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConfigOption { /// The ID of the option - pub id: &'static str, + pub id: String, /// The name of the option - pub name: &'static str, + pub name: String, /// The description of the option - pub description: &'static str, - - /// The table name for the config option - pub table: &'static str, + pub description: String, - /// The common filters to apply to all crud operations on this config options - /// - /// For example, this can be used for guild_id scoped config options or non-guild scoped config options - /// - /// Semantics: - /// - /// View/Update/Delete: Common filters are applied to the view operation as an extension of all other filters - /// Create: Common filters are appended on to the entry itself - pub common_filters: - indexmap::IndexMap>, - - /// The default common filter - pub default_common_filters: indexmap::IndexMap<&'static str, &'static str>, - - /// The primary key of the table - pub primary_key: &'static str, + /// The primary key of the table. Should be present in ID + pub primary_key: String, /// Title template, used for the title of the embed - pub title_template: &'static str, + pub title_template: String, /// The columns for this option pub columns: Arc>, - /// Maximum number of entries to return - /// - /// Only applies to View operations - pub max_return: i64, - - /// Maximum number of entries a server may have - pub max_entries: Option, - /// Operation specific data - pub operations: indexmap::IndexMap, - - /// Any post-operation actions. These are guaranteed to run after the operation has been completed - /// - /// Note: this is pretty useless in View but may be useful in Create/Update/Delete - /// - /// If/when called, the state will be the state after the operation has been completed. In particular, the data itself may not be present in database anymore - pub post_action: Arc, - - /// What validator to use for this config option - pub validator: Arc, + pub supported_operations: Vec, /// The underlying data store to use for fetch operations /// /// This can be useful in cases where postgres/etc. is not the main underlying storage (for example, seaweedfs etc.) - pub data_store: Arc, -} - -impl ConfigOption { - pub fn get_corresponding_command(&self, op: OperationType) -> String { - format!("{} {}", self.id, op.corresponding_command_suffix()) - } + #[serde(skip_serializing_if = "Option::is_none")] + pub executor: OptionSettingExecutor, } impl PartialEq for ConfigOption { @@ -480,157 +250,78 @@ pub fn settings_wrap(v: T) -> Arc { Arc::new(v) } -/// Trait to create a new data store -#[async_trait] -pub trait CreateDataStore: Send + Sync { - /// Create a datastore performing any needed setup - async fn create( - &self, - setting: &ConfigOption, - guild_id: serenity::all::GuildId, - author: serenity::all::UserId, - data: &SettingsData, - common_filters: indexmap::IndexMap, - ) -> Result, SettingsError>; -} - -impl std::fmt::Debug for dyn CreateDataStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "CreateDataStore") - } -} - -/// How should data be fetched -#[async_trait] -pub trait DataStore: Send + Sync { - /// Casts the DataStore to std::any::Any - fn as_any(&mut self) -> &mut dyn std::any::Any; - - /// Start a transaction - async fn start_transaction(&mut self) -> Result<(), SettingsError>; - - /// Commit the changes to the data store - async fn commit(&mut self) -> Result<(), SettingsError>; - - /// Gets the list of all available columns on the database - /// - /// This can be useful for debugging purposes and validation/tests - async fn columns(&mut self) -> Result, SettingsError>; - - /// Fetches all requested fields of a setting for a given guild matching filters - async fn fetch_all( - &mut self, - fields: &[String], - filters: indexmap::IndexMap, - ) -> Result, SettingsError>; - - /// Fetch the count of all entries matching filters - async fn matching_entry_count( - &mut self, - filters: indexmap::IndexMap, - ) -> Result; - - /// Creates a new entry given a set of columns to set returning the newly created entry - async fn create_entry( - &mut self, - entry: indexmap::IndexMap, - ) -> Result; - - /// Updates all matching entry given a set of columns to set and a set of filters - async fn update_matching_entries( - &mut self, - filters: indexmap::IndexMap, - entry: indexmap::IndexMap, - ) -> Result<(), SettingsError>; - - /// Deletes entries given a set of filters - /// - /// NOTE: Data stores should return an error if no rows are deleted - async fn delete_matching_entries( - &mut self, - filters: indexmap::IndexMap, - ) -> Result<(), SettingsError>; -} - -impl std::fmt::Debug for dyn DataStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "DataStore") - } -} - /// This is the context provided to all hooks (post-actions, validators, etc.) #[allow(dead_code)] pub struct HookContext<'a> { pub author: serenity::all::UserId, pub guild_id: serenity::all::GuildId, - pub data_store: &'a mut dyn DataStore, // The current datastore - pub data: &'a SettingsData, // The data object - pub operation_type: OperationType, - pub unchanged_fields: Vec, // The fields that have not changed in an operation + pub data: &'a SettingsData, // The data object } -/// Settings can (optionally) have a validator to allow for custom data validation/processing prior to executing an operation +/// Settings can (optionally) have a post-action to allow for custom data validation/processing prior to executing an operation /// -/// Note that validators are guaranteed to have all data of the column set in state when called +/// Note that post-actions are guaranteed to have all data of the column set in state when called #[async_trait] -pub trait SettingDataValidator: Send + Sync { - /// Validates the data - async fn validate<'a>( +pub trait SettingExecutor: Send + Sync { + /// View the settings data + /// + /// __limit and __offset, if found, contains the limit/offset of the query + /// + /// All Executors should return an __count value containing the total count of the total number of entries + async fn view<'a>(&self, context: HookContext<'a>) -> Result, Error>; + + /// Saves the setting + async fn save<'a>( &self, context: HookContext<'a>, state: &'a mut super::state::State, - ) -> Result<(), SettingsError>; + ) -> Result; + + /// Deletes the setting + async fn delete<'a>( + &self, + context: HookContext<'a>, + pkey: splashcore_rs::value::Value, + ) -> Result; } -impl std::fmt::Debug for dyn SettingDataValidator { +impl std::fmt::Debug for dyn SettingExecutor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SettingDataValidator") + write!(f, "SettingSaver") } } -/// A simple NoOp validator -pub struct NoOpValidator; +#[derive(Clone)] +pub struct OptionSettingExecutor(pub Option>); -#[async_trait] -impl SettingDataValidator for NoOpValidator { - async fn validate<'a>( - &self, - _context: HookContext<'a>, - _state: &'a mut super::state::State, - ) -> Result<(), SettingsError> { - Ok(()) +impl std::ops::Deref for OptionSettingExecutor { + type Target = Option>; + + fn deref(&self) -> &Self::Target { + &self.0 } } -/// Settings can (optionally) have a post-action to allow for custom data validation/processing prior to executing an operation -/// -/// Note that post-actions are guaranteed to have all data of the column set in state when called -#[async_trait] -pub trait PostAction: Send + Sync { - /// Validates the data - async fn post_action<'a>( - &self, - context: HookContext<'a>, - state: &'a mut super::state::State, - ) -> Result<(), SettingsError>; +impl serde::Serialize for OptionSettingExecutor { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + None::>.serialize(serializer) + } } -impl std::fmt::Debug for dyn PostAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "PostAction") +impl<'de> serde::Deserialize<'de> for OptionSettingExecutor { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(OptionSettingExecutor(None)) } } -/// A simple NoOp post-action -pub struct NoOpPostAction; - -#[async_trait] -impl PostAction for NoOpPostAction { - async fn post_action<'a>( - &self, - _context: HookContext<'a>, - _state: &'a mut super::state::State, - ) -> Result<(), SettingsError> { - Ok(()) +impl std::fmt::Debug for OptionSettingExecutor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "OptionSettingExecutor") } } diff --git a/core/rust.std/src/value.rs b/core/rust.std/src/value.rs index cdbb0817..8bc99164 100644 --- a/core/rust.std/src/value.rs +++ b/core/rust.std/src/value.rs @@ -100,7 +100,21 @@ impl Value { #[allow(dead_code)] pub fn from_json(value: &serde_json::Value) -> Self { match value { - serde_json::Value::String(s) => Self::String(s.clone()), + serde_json::Value::String(s) => { + let t = chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S"); + + if let Ok(t) = t { + return Self::Timestamp(t); + } + + let value = chrono::DateTime::parse_from_rfc3339(&s); + + if let Ok(value) = value { + return Self::TimestampTz(value.into()); + } + + Self::String(s.clone()) + } serde_json::Value::Number(n) => { if n.is_i64() { Self::Integer(n.as_i64().unwrap()) @@ -477,3 +491,22 @@ impl Value { matches!(self, Value::None) } } + +impl serde::Serialize for Value { + fn serialize(&self, serializer: S) -> Result { + let value = self.to_json(); + + value.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Value { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + + Ok(Value::from_json(&value)) + } +}