diff --git a/src/basic/keywords/mod.rs b/src/basic/keywords/mod.rs index e0645fcec..217aad607 100644 --- a/src/basic/keywords/mod.rs +++ b/src/basic/keywords/mod.rs @@ -26,6 +26,7 @@ pub mod face_api; pub mod file_operations; pub mod find; pub mod first; +pub mod products; pub mod search; pub mod for_next; pub mod format; @@ -125,6 +126,9 @@ pub fn get_all_keywords() -> Vec { "FIND".to_string(), "FIRST".to_string(), "SEARCH".to_string(), + "SEARCH PRODUCTS".to_string(), + "PRODUCTS".to_string(), + "PRODUCT".to_string(), "AUTOCOMPLETE".to_string(), "GROUP BY".to_string(), "INSERT".to_string(), diff --git a/src/basic/keywords/products.rs b/src/basic/keywords/products.rs new file mode 100644 index 000000000..cc2a39427 --- /dev/null +++ b/src/basic/keywords/products.rs @@ -0,0 +1,277 @@ +use crate::bot::get_default_bot; +use crate::core::shared::schema::products; +use crate::shared::models::UserSession; +use crate::shared::state::AppState; +use crate::shared::utils; +use diesel::prelude::*; +use diesel::sql_types::{Integer, Text}; +use log::{error, trace}; +use rhai::{Dynamic, Engine}; +use serde_json::{json, Value}; +use std::sync::Arc; + +#[derive(QueryableByName)] +struct JsonRow { + #[diesel(sql_type = Text)] + row_data: String, +} + +pub fn products_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { + let connection = state.conn.clone(); + + engine.register_fn("PRODUCTS", { + let conn = connection.clone(); + move || -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("PRODUCTS db error: {e}"); + return Dynamic::from(rhai::Array::new()); + } + }; + + match get_all_products(&mut binding) { + Ok(products) => utils::json_value_to_dynamic(&products), + Err(e) => { + error!("PRODUCTS error: {e}"); + Dynamic::from(rhai::Array::new()) + } + } + } + }); + + engine.register_fn("products", { + let conn = connection.clone(); + move || -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("products db error: {e}"); + return Dynamic::from(rhai::Array::new()); + } + }; + + match get_all_products(&mut binding) { + Ok(products) => utils::json_value_to_dynamic(&products), + Err(e) => { + error!("products error: {e}"); + Dynamic::from(rhai::Array::new()) + } + } + } + }); + + engine.register_fn("PRODUCT", { + let conn = connection.clone(); + move |id: i64| -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("PRODUCT db error: {e}"); + return Dynamic::UNIT; + } + }; + + match get_product_by_id(&mut binding, id) { + Ok(Some(product)) => utils::json_value_to_dynamic(&product), + Ok(None) => Dynamic::UNIT, + Err(e) => { + error!("PRODUCT error: {e}"); + Dynamic::UNIT + } + } + } + }); + + engine.register_fn("product", { + let conn = connection.clone(); + move |id: i64| -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("product db error: {e}"); + return Dynamic::UNIT; + } + }; + + match get_product_by_id(&mut binding, id) { + Ok(Some(product)) => utils::json_value_to_dynamic(&product), + Ok(None) => Dynamic::UNIT, + Err(e) => { + error!("product error: {e}"); + Dynamic::UNIT + } + } + } + }); + + engine + .register_custom_syntax( + ["SEARCH", "PRODUCTS", "$expr$", ",", "$expr$"], + false, + { + let conn = connection.clone(); + move |context, inputs| { + let query = context.eval_expression_tree(&inputs[0])?; + let limit = context.eval_expression_tree(&inputs[1])?; + + let mut binding = conn.get().map_err(|e| format!("DB error: {e}"))?; + let query_str = query.to_string(); + let limit_val = limit.as_int().unwrap_or(10) as i32; + + let result = search_products(&mut binding, &query_str, limit_val) + .map_err(|e| format!("Search error: {e}"))?; + + Ok(utils::json_value_to_dynamic(&result)) + } + }, + ) + .expect("valid syntax"); + + engine + .register_custom_syntax(["SEARCH", "PRODUCTS", "$expr$"], false, { + let conn = connection.clone(); + move |context, inputs| { + let query = context.eval_expression_tree(&inputs[0])?; + + let mut binding = conn.get().map_err(|e| format!("DB error: {e}"))?; + let query_str = query.to_string(); + + let result = search_products(&mut binding, &query_str, 10) + .map_err(|e| format!("Search error: {e}"))?; + + Ok(utils::json_value_to_dynamic(&result)) + } + }) + .expect("valid syntax"); + + engine.register_fn("SEARCH_PRODUCTS", { + let conn = connection.clone(); + move |query: String, limit: i64| -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("SEARCH_PRODUCTS db error: {e}"); + return Dynamic::from(rhai::Array::new()); + } + }; + + match search_products(&mut binding, &query, limit as i32) { + Ok(products) => utils::json_value_to_dynamic(&products), + Err(e) => { + error!("SEARCH_PRODUCTS error: {e}"); + Dynamic::from(rhai::Array::new()) + } + } + } + }); + + engine.register_fn("search_products", { + let conn = connection.clone(); + move |query: String, limit: i64| -> Dynamic { + let mut binding = match conn.get() { + Ok(c) => c, + Err(e) => { + error!("search_products db error: {e}"); + return Dynamic::from(rhai::Array::new()); + } + }; + + match search_products(&mut binding, &query, limit as i32) { + Ok(products) => utils::json_value_to_dynamic(&products), + Err(e) => { + error!("search_products error: {e}"); + Dynamic::from(rhai::Array::new()) + } + } + } + }); +} + +fn get_all_products(conn: &mut diesel::PgConnection) -> Result { + trace!("get_all_products"); + + let (bot_id, _) = get_default_bot(conn); + + let query = r#" + SELECT row_to_json(p)::text as row_data + FROM products p + WHERE p.bot_id = $1 AND p.is_active = true + ORDER BY p.name + LIMIT 100 + "#; + + let results: Vec = diesel::sql_query(query) + .bind::(bot_id) + .load(conn) + .map_err(|e| e.to_string())?; + + let products: Vec = results + .into_iter() + .filter_map(|row| serde_json::from_str(&row.row_data).ok()) + .collect(); + + Ok(json!(products)) +} + +fn get_product_by_id(conn: &mut diesel::PgConnection, id: i64) -> Result, String> { + trace!("get_product_by_id: {id}"); + + let query = r#" + SELECT row_to_json(p)::text as row_data + FROM products p + WHERE p.id = $1 + LIMIT 1 + "#; + + let uuid = uuid::Uuid::from_u64_pair(0, id as u64); + + let results: Vec = diesel::sql_query(query) + .bind::(uuid) + .load(conn) + .map_err(|e| e.to_string())?; + + Ok(results + .into_iter() + .next() + .and_then(|row| serde_json::from_str(&row.row_data).ok())) +} + +fn search_products(conn: &mut diesel::PgConnection, query: &str, limit: i32) -> Result { + trace!("search_products: query={query}, limit={limit}"); + + let (bot_id, _) = get_default_bot(conn); + let safe_query = query.replace('\'', "''"); + + let sql = r#" + SELECT row_to_json(p)::text as row_data + FROM products p + WHERE p.bot_id = $1 + AND p.is_active = true + AND ( + p.name ILIKE '%' || $2 || '%' + OR p.description ILIKE '%' || $2 || '%' + OR p.sku ILIKE '%' || $2 || '%' + OR p.brand ILIKE '%' || $2 || '%' + OR p.category ILIKE '%' || $2 || '%' + ) + ORDER BY + CASE WHEN p.name ILIKE $2 || '%' THEN 0 ELSE 1 END, + p.name + LIMIT $3 + "#; + + let results: Vec = diesel::sql_query(sql) + .bind::(bot_id) + .bind::(&safe_query) + .bind::(limit) + .load(conn) + .map_err(|e| e.to_string())?; + + let products: Vec = results + .into_iter() + .filter_map(|row| serde_json::from_str(&row.row_data).ok()) + .collect(); + + Ok(json!(products)) +} diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 7dde696a8..2ab063163 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -35,6 +35,7 @@ use self::keywords::data_operations::register_data_operations; use self::keywords::file_operations::register_file_operations; use self::keywords::find::find_keyword; use self::keywords::search::search_keyword; +use self::keywords::products::products_keyword; use self::keywords::first::first_keyword; use self::keywords::for_next::for_keyword; use self::keywords::format::format_keyword; @@ -88,6 +89,7 @@ impl ScriptService { create_site_keyword(&state, user.clone(), &mut engine); find_keyword(&state, user.clone(), &mut engine); search_keyword(&state, user.clone(), &mut engine); + products_keyword(&state, user.clone(), &mut engine); for_keyword(&state, user.clone(), &mut engine); let _ = register_use_kb_keyword(&mut engine, state.clone(), Arc::new(user.clone())); let _ = register_clear_kb_keyword(&mut engine, state.clone(), Arc::new(user.clone()));