This commit is contained in:
parent
9a4bab6de6
commit
9aecbfc6fb
13 changed files with 460 additions and 394 deletions
|
@ -48,6 +48,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
|
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
.send_wildcard()
|
.send_wildcard()
|
||||||
.allowed_origin("*")
|
.allowed_origin("*")
|
||||||
|
@ -55,6 +56,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
||||||
.allowed_header(header::CONTENT_TYPE)
|
.allowed_header(header::CONTENT_TYPE)
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.app_data(app_state.clone())
|
.app_data(app_state.clone())
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod utils;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod keywords;
|
pub mod keywords;
|
||||||
|
|
32
src/services/keywords/create_draft.rs
Normal file
32
src/services/keywords/create_draft.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
pub fn create_draft_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"],
|
||||||
|
true, // Statement
|
||||||
|
|context, inputs| {
|
||||||
|
if inputs.len() < 3 {
|
||||||
|
return Err("Not enough arguments for CREATE DRAFT".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let to = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
let subject = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
let body = context.eval_expression_tree(&inputs[2])?;
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"command": "create_draft",
|
||||||
|
"to": to.to_string(),
|
||||||
|
"subject": subject.to_string(),
|
||||||
|
"body": body.to_string()
|
||||||
|
});
|
||||||
|
println!("CREATE DRAFT executed: {}", result.to_string());
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
40
src/services/keywords/create_site.rs
Normal file
40
src/services/keywords/create_site.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&[
|
||||||
|
"CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",",
|
||||||
|
"$expr$",
|
||||||
|
],
|
||||||
|
true, // Statement
|
||||||
|
|context, inputs| {
|
||||||
|
if inputs.len() < 5 {
|
||||||
|
return Err("Not enough arguments for CREATE SITE".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
let company = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
let website = context.eval_expression_tree(&inputs[2])?;
|
||||||
|
let template = context.eval_expression_tree(&inputs[3])?;
|
||||||
|
let prompt = context.eval_expression_tree(&inputs[4])?;
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"command": "create_site",
|
||||||
|
"name": name.to_string(),
|
||||||
|
"company": company.to_string(),
|
||||||
|
"website": website.to_string(),
|
||||||
|
"template": template.to_string(),
|
||||||
|
"prompt": prompt.to_string()
|
||||||
|
});
|
||||||
|
println!("CREATE SITE executed: {}", result.to_string());
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -1,35 +1,79 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use sqlx::Column; // Required for .name() method
|
use sqlx::{PgPool};
|
||||||
use sqlx::TypeInfo; // Required for .type_info() method
|
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
use crate::services::utils;
|
||||||
|
use crate::services::utils::row_to_json;
|
||||||
|
use crate::services::utils::to_array;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn find_keyword(state: &AppState, engine: &mut Engine) {
|
||||||
|
let db = state.db_custom.clone();
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, {
|
||||||
|
let db = db.clone();
|
||||||
|
|
||||||
|
move |context, inputs| {
|
||||||
|
let table_name = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
let filter = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
let binding = db.as_ref().unwrap();
|
||||||
|
|
||||||
|
// Use the current async context instead of creating a new runtime
|
||||||
|
let binding2 = table_name.to_string();
|
||||||
|
let binding3 = filter.to_string();
|
||||||
|
let fut = execute_find(binding, &binding2, &binding3);
|
||||||
|
|
||||||
|
// Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on
|
||||||
|
let result =
|
||||||
|
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
|
||||||
|
.map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
|
||||||
|
if let Some(results) = result.get("results") {
|
||||||
|
let array = to_array(utils::json_value_to_dynamic(results));
|
||||||
|
Ok(Dynamic::from(array))
|
||||||
|
} else {
|
||||||
|
Err("No results".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn execute_find(
|
pub async fn execute_find(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
table_str: &str,
|
table_str: &str,
|
||||||
filter_str: &str,
|
filter_str: &str,
|
||||||
) -> Result<Value, String> { // Changed to String error like your Actix code
|
) -> Result<Value, String> {
|
||||||
println!("Starting execute_find with table: {}, filter: {}", table_str, filter_str);
|
// Changed to String error like your Actix code
|
||||||
|
println!(
|
||||||
let (where_clause, params) = parse_filter(filter_str)
|
"Starting execute_find with table: {}, filter: {}",
|
||||||
.map_err(|e| e.to_string())?;
|
table_str, filter_str
|
||||||
|
);
|
||||||
let query = format!("SELECT * FROM {} WHERE {} LIMIT 10", table_str, where_clause);
|
|
||||||
|
let (where_clause, params) = parse_filter(filter_str).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT * FROM {} WHERE {} LIMIT 10",
|
||||||
|
table_str, where_clause
|
||||||
|
);
|
||||||
println!("Executing query: {}", query);
|
println!("Executing query: {}", query);
|
||||||
|
|
||||||
// Use the same simple pattern as your Actix code - no timeout wrapper
|
// Use the same simple pattern as your Actix code - no timeout wrapper
|
||||||
let rows = sqlx::query(&query)
|
let rows = sqlx::query(&query)
|
||||||
.bind(¶ms[0]) // Simplified like your working code
|
.bind(¶ms[0]) // Simplified like your working code
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
eprintln!("SQL execution error: {}", e);
|
eprintln!("SQL execution error: {}", e);
|
||||||
e.to_string()
|
e.to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
println!("Query successful, got {} rows", rows.len());
|
println!("Query successful, got {} rows", rows.len());
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
results.push(row_to_json(row).map_err(|e| e.to_string())?);
|
results.push(row_to_json(row).map_err(|e| e.to_string())?);
|
||||||
|
@ -43,96 +87,6 @@ pub async fn execute_find(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_json(row: PgRow) -> Result<Value, Box<dyn Error>> {
|
|
||||||
let mut result = serde_json::Map::new();
|
|
||||||
let columns = row.columns();
|
|
||||||
println!("Processing {} columns", columns.len());
|
|
||||||
|
|
||||||
for (i, column) in columns.iter().enumerate() {
|
|
||||||
let column_name = column.name();
|
|
||||||
let type_name = column.type_info().name();
|
|
||||||
println!(
|
|
||||||
"Processing column {}: {} (type: {})",
|
|
||||||
i, column_name, type_name
|
|
||||||
);
|
|
||||||
|
|
||||||
let value: Value = match type_name {
|
|
||||||
"INT4" | "INT8" | "int4" | "int8" => match row.try_get::<i64, _>(i) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Got int64 value: {}", v);
|
|
||||||
json!(v)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get int64, trying i32: {}", e);
|
|
||||||
match row.try_get::<i32, _>(i) {
|
|
||||||
Ok(v) => json!(v as i64),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FLOAT4" | "FLOAT8" | "float4" | "float8" => match row.try_get::<f64, _>(i) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Got float64 value: {}", v);
|
|
||||||
json!(v)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get float64, trying f32: {}", e);
|
|
||||||
match row.try_get::<f32, _>(i) {
|
|
||||||
Ok(v) => json!(v as f64),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TEXT" | "VARCHAR" | "text" | "varchar" => match row.try_get::<String, _>(i) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Got string value: {}", v);
|
|
||||||
json!(v)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get string: {}", e);
|
|
||||||
Value::Null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"BOOL" | "bool" => match row.try_get::<bool, _>(i) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Got bool value: {}", v);
|
|
||||||
json!(v)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get bool: {}", e);
|
|
||||||
Value::Null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"JSON" | "JSONB" | "json" | "jsonb" => match row.try_get::<Value, _>(i) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Got JSON value: {:?}", v);
|
|
||||||
v
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get JSON, trying as string: {}", e);
|
|
||||||
match row.try_get::<String, _>(i) {
|
|
||||||
Ok(s) => match serde_json::from_str(&s) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => json!(s),
|
|
||||||
},
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
println!("Unknown type {}, trying as string", type_name);
|
|
||||||
match row.try_get::<String, _>(i) {
|
|
||||||
Ok(v) => json!(v),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.insert(column_name.to_string(), value);
|
|
||||||
}
|
|
||||||
println!("Finished processing row, got {} fields", result.len());
|
|
||||||
Ok(Value::Object(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
||||||
fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||||
let parts: Vec<&str> = filter_str.split('=').collect();
|
let parts: Vec<&str> = filter_str.split('=').collect();
|
||||||
|
|
83
src/services/keywords/for_next.rs
Normal file
83
src/services/keywords/for_next.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
pub fn for_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| {
|
||||||
|
Err("EXIT FOR".into())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&[
|
||||||
|
"FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$",
|
||||||
|
],
|
||||||
|
true, // We're modifying the scope by adding the loop variable
|
||||||
|
|context, inputs| {
|
||||||
|
// Get the iterator variable names
|
||||||
|
let loop_var = inputs[0].get_string_value().unwrap();
|
||||||
|
let next_var = inputs[3].get_string_value().unwrap();
|
||||||
|
|
||||||
|
// Verify variable names match
|
||||||
|
if loop_var != next_var {
|
||||||
|
return Err(format!(
|
||||||
|
"NEXT variable '{}' doesn't match FOR EACH variable '{}'",
|
||||||
|
next_var, loop_var
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the collection expression
|
||||||
|
let collection = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
|
||||||
|
// Debug: Print the collection type
|
||||||
|
println!("Collection type: {}", collection.type_name());
|
||||||
|
let ccc = collection.clone();
|
||||||
|
// Convert to array - with proper error handling
|
||||||
|
let array = match collection.into_array() {
|
||||||
|
Ok(arr) => arr,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!(
|
||||||
|
"foreach expected array, got {}: {}",
|
||||||
|
ccc.type_name(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Get the block as an expression tree
|
||||||
|
let block = &inputs[2];
|
||||||
|
|
||||||
|
// Remember original scope length
|
||||||
|
let orig_len = context.scope().len();
|
||||||
|
|
||||||
|
for item in array {
|
||||||
|
// Push the loop variable into the scope
|
||||||
|
context.scope_mut().push(loop_var, item);
|
||||||
|
|
||||||
|
// Evaluate the block with the current scope
|
||||||
|
match context.eval_expression_tree(block) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) if e.to_string() == "EXIT FOR" => {
|
||||||
|
context.scope_mut().rewind(orig_len);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Rewind the scope before returning error
|
||||||
|
context.scope_mut().rewind(orig_len);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the loop variable for next iteration
|
||||||
|
context.scope_mut().rewind(orig_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
20
src/services/keywords/get.rs
Normal file
20
src/services/keywords/get.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
pub fn get_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&["GET", "$expr$"],
|
||||||
|
false, // Expression, not statement
|
||||||
|
|context, inputs| {
|
||||||
|
let url = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
let url_str = url.to_string();
|
||||||
|
|
||||||
|
println!("GET executed: {}", url_str.to_string());
|
||||||
|
Ok(format!("Content from {}", url_str).into())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -1 +1,7 @@
|
||||||
pub mod find;
|
pub mod create_draft;
|
||||||
|
pub mod create_site;
|
||||||
|
pub mod find;
|
||||||
|
pub mod for_next;
|
||||||
|
pub mod get;
|
||||||
|
pub mod print;
|
||||||
|
pub mod set;
|
20
src/services/keywords/print.rs
Normal file
20
src/services/keywords/print.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
pub fn print_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
|
||||||
|
// PRINT command
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&["PRINT", "$expr$"],
|
||||||
|
true, // Statement
|
||||||
|
|context, inputs| {
|
||||||
|
let value = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
println!("{}", value);
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
36
src/services/keywords/set.rs
Normal file
36
src/services/keywords/set.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::Engine;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn set_keyword(_state: &AppState, engine: &mut Engine) {
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
&["SET", "$expr$", ",", "$expr$", ",", "$expr$"],
|
||||||
|
true, // Statement
|
||||||
|
|context, inputs| {
|
||||||
|
let table_name = context.eval_expression_tree(&inputs[0])?;
|
||||||
|
let key_value = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
let value = context.eval_expression_tree(&inputs[2])?;
|
||||||
|
|
||||||
|
let table_str = table_name.to_string();
|
||||||
|
let key_str = key_value.to_string();
|
||||||
|
let value_str = value.to_string();
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"command": "set",
|
||||||
|
"status": "success",
|
||||||
|
"table": table_str,
|
||||||
|
"key": key_str,
|
||||||
|
"value": value_str
|
||||||
|
});
|
||||||
|
println!("SET executed: {}", result.to_string());
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
}
|
|
@ -1,301 +1,33 @@
|
||||||
use rhai::{Array, Dynamic, Engine};
|
use rhai::{ Dynamic, Engine, EvalAltResult};
|
||||||
use rhai::{EvalAltResult};
|
use crate::services::keywords::create_draft::{create_draft_keyword};
|
||||||
use serde_json::{json, Value};
|
use crate::services::keywords::create_site::create_site_keyword;
|
||||||
use smartstring::SmartString;
|
use crate::services::keywords::find::{find_keyword};
|
||||||
|
use crate::services::keywords::for_next::for_keyword;
|
||||||
use crate::services::keywords::find::execute_find;
|
use crate::services::keywords::get::get_keyword;
|
||||||
|
use crate::services::keywords::print::print_keyword;
|
||||||
|
use crate::services::keywords::set::set_keyword;
|
||||||
use crate::services::state::AppState;
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
pub struct ScriptService {
|
pub struct ScriptService {
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
|
||||||
match value {
|
|
||||||
Value::Null => Dynamic::UNIT,
|
|
||||||
Value::Bool(b) => Dynamic::from(*b),
|
|
||||||
Value::Number(n) => {
|
|
||||||
if let Some(i) = n.as_i64() {
|
|
||||||
Dynamic::from(i)
|
|
||||||
} else if let Some(f) = n.as_f64() {
|
|
||||||
Dynamic::from(f)
|
|
||||||
} else {
|
|
||||||
Dynamic::UNIT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::String(s) => Dynamic::from(s.clone()),
|
|
||||||
Value::Array(arr) => Dynamic::from(
|
|
||||||
arr.iter()
|
|
||||||
.map(json_value_to_dynamic)
|
|
||||||
.collect::<rhai::Array>(),
|
|
||||||
),
|
|
||||||
Value::Object(obj) => Dynamic::from(
|
|
||||||
obj.iter()
|
|
||||||
.map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v)))
|
|
||||||
.collect::<rhai::Map>(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts any value to an array - single values become single-element arrays
|
|
||||||
fn to_array(value: Dynamic) -> Array {
|
|
||||||
if value.is_array() {
|
|
||||||
// Already an array - return as-is
|
|
||||||
value.cast::<Array>()
|
|
||||||
} else if value.is_unit() || value.is::<()>() {
|
|
||||||
// Handle empty/unit case
|
|
||||||
Array::new()
|
|
||||||
} else {
|
|
||||||
// Convert single value to single-element array
|
|
||||||
Array::from([value])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptService {
|
impl ScriptService {
|
||||||
pub fn new(state: &AppState) -> Self {
|
pub fn new(state: &AppState) -> Self {
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
// Configure engine for BASIC-like syntax
|
// Configure engine for BASIC-like syntax
|
||||||
engine.set_allow_anonymous_fn(true);
|
engine.set_allow_anonymous_fn(true);
|
||||||
engine.set_allow_looping(true);
|
engine.set_allow_looping(true);
|
||||||
|
|
||||||
engine
|
create_draft_keyword(state, &mut engine);
|
||||||
.register_custom_syntax(
|
create_site_keyword(state, &mut engine);
|
||||||
&[
|
find_keyword(state, &mut engine);
|
||||||
"FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$",
|
for_keyword(state, &mut engine);
|
||||||
],
|
get_keyword(state, &mut engine);
|
||||||
true, // We're modifying the scope by adding the loop variable
|
set_keyword(state, &mut engine);
|
||||||
|context, inputs| {
|
print_keyword(state, &mut engine);
|
||||||
// Get the iterator variable names
|
ScriptService { engine }
|
||||||
let loop_var = inputs[0].get_string_value().unwrap();
|
|
||||||
let next_var = inputs[3].get_string_value().unwrap();
|
|
||||||
|
|
||||||
// Verify variable names match
|
|
||||||
if loop_var != next_var {
|
|
||||||
return Err(format!(
|
|
||||||
"NEXT variable '{}' doesn't match FOR EACH variable '{}'",
|
|
||||||
next_var, loop_var
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate the collection expression
|
|
||||||
let collection = context.eval_expression_tree(&inputs[1])?;
|
|
||||||
|
|
||||||
// Debug: Print the collection type
|
|
||||||
println!("Collection type: {}", collection.type_name());
|
|
||||||
let ccc = collection.clone();
|
|
||||||
// Convert to array - with proper error handling
|
|
||||||
let array = match collection.into_array() {
|
|
||||||
Ok(arr) => arr,
|
|
||||||
Err(err) => {
|
|
||||||
return Err(format!(
|
|
||||||
"foreach expected array, got {}: {}",
|
|
||||||
ccc.type_name(),
|
|
||||||
err
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Get the block as an expression tree
|
|
||||||
let block = &inputs[2];
|
|
||||||
|
|
||||||
// Remember original scope length
|
|
||||||
let orig_len = context.scope().len();
|
|
||||||
|
|
||||||
for item in array {
|
|
||||||
// Push the loop variable into the scope
|
|
||||||
context.scope_mut().push(loop_var, item);
|
|
||||||
|
|
||||||
// Evaluate the block with the current scope
|
|
||||||
match context.eval_expression_tree(block) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(e) if e.to_string() == "EXIT FOR" => {
|
|
||||||
context.scope_mut().rewind(orig_len);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Rewind the scope before returning error
|
|
||||||
context.scope_mut().rewind(orig_len);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the loop variable for next iteration
|
|
||||||
context.scope_mut().rewind(orig_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Dynamic::UNIT)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Register EXIT FOR
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| {
|
|
||||||
Err("EXIT FOR".into())
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// FIND command: FIND "table", "filter"
|
|
||||||
// Clone the database reference outside the closure to avoid lifetime issues
|
|
||||||
let db = state.db_custom.clone();
|
|
||||||
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, {
|
|
||||||
let db = db.clone();
|
|
||||||
|
|
||||||
move |context, inputs| {
|
|
||||||
let table_name = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
let filter = context.eval_expression_tree(&inputs[1])?;
|
|
||||||
let binding = db.as_ref().unwrap();
|
|
||||||
|
|
||||||
// Use the current async context instead of creating a new runtime
|
|
||||||
let binding2 = table_name.to_string();
|
|
||||||
let binding3 = filter.to_string();
|
|
||||||
let fut = execute_find(
|
|
||||||
binding,
|
|
||||||
&binding2,
|
|
||||||
&binding3,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on
|
|
||||||
let result = tokio::task::block_in_place(|| {
|
|
||||||
tokio::runtime::Handle::current().block_on(fut)
|
|
||||||
})
|
|
||||||
.map_err(|e| format!("DB error: {}", e))?;
|
|
||||||
|
|
||||||
if let Some(results) = result.get("results") {
|
|
||||||
let array = to_array(json_value_to_dynamic(results));
|
|
||||||
Ok(Dynamic::from(array))
|
|
||||||
} else {
|
|
||||||
Err("No results".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// SET command: SET "table", "key", "value"
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(
|
|
||||||
&["SET", "$expr$", ",", "$expr$", ",", "$expr$"],
|
|
||||||
true, // Statement
|
|
||||||
|context, inputs| {
|
|
||||||
let table_name = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
let key_value = context.eval_expression_tree(&inputs[1])?;
|
|
||||||
let value = context.eval_expression_tree(&inputs[2])?;
|
|
||||||
|
|
||||||
let table_str = table_name.to_string();
|
|
||||||
let key_str = key_value.to_string();
|
|
||||||
let value_str = value.to_string();
|
|
||||||
|
|
||||||
let result = json!({
|
|
||||||
"command": "set",
|
|
||||||
"status": "success",
|
|
||||||
"table": table_str,
|
|
||||||
"key": key_str,
|
|
||||||
"value": value_str
|
|
||||||
});
|
|
||||||
println!("SET executed: {}", result.to_string());
|
|
||||||
Ok(Dynamic::UNIT)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// GET command: GET "url"
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(
|
|
||||||
&["GET", "$expr$"],
|
|
||||||
false, // Expression, not statement
|
|
||||||
|context, inputs| {
|
|
||||||
let url = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
let url_str = url.to_string();
|
|
||||||
|
|
||||||
println!("GET executed: {}", url_str.to_string());
|
|
||||||
Ok(format!("Content from {}", url_str).into())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// CREATE SITE command: CREATE SITE "name", "company", "website", "template", "prompt"
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(
|
|
||||||
&[
|
|
||||||
"CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",",
|
|
||||||
"$expr$",
|
|
||||||
],
|
|
||||||
true, // Statement
|
|
||||||
|context, inputs| {
|
|
||||||
if inputs.len() < 5 {
|
|
||||||
return Err("Not enough arguments for CREATE SITE".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
let company = context.eval_expression_tree(&inputs[1])?;
|
|
||||||
let website = context.eval_expression_tree(&inputs[2])?;
|
|
||||||
let template = context.eval_expression_tree(&inputs[3])?;
|
|
||||||
let prompt = context.eval_expression_tree(&inputs[4])?;
|
|
||||||
|
|
||||||
let result = json!({
|
|
||||||
"command": "create_site",
|
|
||||||
"name": name.to_string(),
|
|
||||||
"company": company.to_string(),
|
|
||||||
"website": website.to_string(),
|
|
||||||
"template": template.to_string(),
|
|
||||||
"prompt": prompt.to_string()
|
|
||||||
});
|
|
||||||
println!("CREATE SITE executed: {}", result.to_string());
|
|
||||||
Ok(Dynamic::UNIT)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// CREATE DRAFT command: CREATE DRAFT "to", "subject", "body"
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(
|
|
||||||
&["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"],
|
|
||||||
true, // Statement
|
|
||||||
|context, inputs| {
|
|
||||||
if inputs.len() < 3 {
|
|
||||||
return Err("Not enough arguments for CREATE DRAFT".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let to = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
let subject = context.eval_expression_tree(&inputs[1])?;
|
|
||||||
let body = context.eval_expression_tree(&inputs[2])?;
|
|
||||||
|
|
||||||
let result = json!({
|
|
||||||
"command": "create_draft",
|
|
||||||
"to": to.to_string(),
|
|
||||||
"subject": subject.to_string(),
|
|
||||||
"body": body.to_string()
|
|
||||||
});
|
|
||||||
println!("CREATE DRAFT executed: {}", result.to_string());
|
|
||||||
Ok(Dynamic::UNIT)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// PRINT command
|
|
||||||
engine
|
|
||||||
.register_custom_syntax(
|
|
||||||
&["PRINT", "$expr$"],
|
|
||||||
true, // Statement
|
|
||||||
|context, inputs| {
|
|
||||||
let value = context.eval_expression_tree(&inputs[0])?;
|
|
||||||
println!("{}", value);
|
|
||||||
Ok(Dynamic::UNIT)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Register web service functions
|
|
||||||
engine.register_fn("web_get", |url: &str| format!("Response from {}", url));
|
|
||||||
|
|
||||||
ScriptService {
|
|
||||||
engine,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_basic_script(&self, script: &str) -> String {
|
fn preprocess_basic_script(&self, script: &str) -> String {
|
||||||
|
@ -370,7 +102,7 @@ impl ScriptService {
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
result.push(';');
|
result.push(';');
|
||||||
} else {
|
} else {
|
||||||
// Add semicolons only for non-BASIC statements
|
// Add semicolons only for BASIC statements
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
|
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
|
||||||
result.push(';');
|
result.push(';');
|
||||||
|
|
|
@ -8,6 +8,6 @@ pub struct AppState {
|
||||||
pub minio_client: Option<Client>,
|
pub minio_client: Option<Client>,
|
||||||
pub config: Option<AppConfig>,
|
pub config: Option<AppConfig>,
|
||||||
pub db: Option<sqlx::PgPool>,
|
pub db: Option<sqlx::PgPool>,
|
||||||
pub db_custom: Option<sqlx::PgPool>,
|
pub db_custom: Option<sqlx::PgPool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
141
src/services/utils.rs
Normal file
141
src/services/utils.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use smartstring::SmartString;
|
||||||
|
use rhai::{Array, Dynamic};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sqlx::Column; // Required for .name() method
|
||||||
|
use sqlx::TypeInfo; // Required for .type_info() method
|
||||||
|
use sqlx::{postgres::PgRow, Row};
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub fn row_to_json(row: PgRow) -> Result<Value, Box<dyn Error>> {
|
||||||
|
let mut result = serde_json::Map::new();
|
||||||
|
let columns = row.columns();
|
||||||
|
println!("Processing {} columns", columns.len());
|
||||||
|
|
||||||
|
for (i, column) in columns.iter().enumerate() {
|
||||||
|
let column_name = column.name();
|
||||||
|
let type_name = column.type_info().name();
|
||||||
|
println!(
|
||||||
|
"Processing column {}: {} (type: {})",
|
||||||
|
i, column_name, type_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let value: Value = match type_name {
|
||||||
|
"INT4" | "INT8" | "int4" | "int8" => match row.try_get::<i64, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got int64 value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get int64, trying i32: {}", e);
|
||||||
|
match row.try_get::<i32, _>(i) {
|
||||||
|
Ok(v) => json!(v as i64),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FLOAT4" | "FLOAT8" | "float4" | "float8" => match row.try_get::<f64, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got float64 value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get float64, trying f32: {}", e);
|
||||||
|
match row.try_get::<f32, _>(i) {
|
||||||
|
Ok(v) => json!(v as f64),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TEXT" | "VARCHAR" | "text" | "varchar" => match row.try_get::<String, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got string value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get string: {}", e);
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BOOL" | "bool" => match row.try_get::<bool, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got bool value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get bool: {}", e);
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"JSON" | "JSONB" | "json" | "jsonb" => match row.try_get::<Value, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got JSON value: {:?}", v);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get JSON, trying as string: {}", e);
|
||||||
|
match row.try_get::<String, _>(i) {
|
||||||
|
Ok(s) => match serde_json::from_str(&s) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => json!(s),
|
||||||
|
},
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("Unknown type {}, trying as string", type_name);
|
||||||
|
match row.try_get::<String, _>(i) {
|
||||||
|
Ok(v) => json!(v),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.insert(column_name.to_string(), value);
|
||||||
|
}
|
||||||
|
println!("Finished processing row, got {} fields", result.len());
|
||||||
|
Ok(Value::Object(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
||||||
|
match value {
|
||||||
|
Value::Null => Dynamic::UNIT,
|
||||||
|
Value::Bool(b) => Dynamic::from(*b),
|
||||||
|
Value::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
Dynamic::from(i)
|
||||||
|
} else if let Some(f) = n.as_f64() {
|
||||||
|
Dynamic::from(f)
|
||||||
|
} else {
|
||||||
|
Dynamic::UNIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::String(s) => Dynamic::from(s.clone()),
|
||||||
|
Value::Array(arr) => Dynamic::from(
|
||||||
|
arr.iter()
|
||||||
|
.map(json_value_to_dynamic)
|
||||||
|
.collect::<rhai::Array>(),
|
||||||
|
),
|
||||||
|
Value::Object(obj) => Dynamic::from(
|
||||||
|
obj.iter()
|
||||||
|
.map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v)))
|
||||||
|
.collect::<rhai::Map>(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts any value to an array - single values become single-element arrays
|
||||||
|
pub fn to_array(value: Dynamic) -> Array {
|
||||||
|
if value.is_array() {
|
||||||
|
// Already an array - return as-is
|
||||||
|
value.cast::<Array>()
|
||||||
|
} else if value.is_unit() || value.is::<()>() {
|
||||||
|
// Handle empty/unit case
|
||||||
|
Array::new()
|
||||||
|
} else {
|
||||||
|
// Convert single value to single-element array
|
||||||
|
Array::from([value])
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue