feat: unified keywords with spaces, ON ERROR RESUME NEXT, unified DELETE
Keywords now use spaces instead of underscores: - SEND MAIL (was SEND_MAIL) - GENERATE PDF (was GENERATE_PDF) - MERGE PDF (was MERGE_PDF) - SET HEADER (was SET_HEADER) - CLEAR HEADERS (was CLEAR_HEADERS) New ON ERROR RESUME NEXT implementation: - ON ERROR RESUME NEXT - enable error trapping - ON ERROR GOTO 0 - disable error trapping - CLEAR ERROR - clear error state - ERROR MESSAGE - get last error message - ERR - get error number Unified DELETE keyword: - DELETE url - HTTP DELETE (auto-detected) - DELETE table, filter - Database DELETE - DELETE path - File DELETE Changes: - errors/on_error.rs: New VB-style error handling - errors/mod.rs: Include on_error module - send_mail.rs: SEND MAIL with spaces - file_operations.rs: GENERATE PDF, MERGE PDF with spaces - data_operations.rs: Unified DELETE with auto-detection - http_operations.rs: Cleaned up DELETE HTTP - compiler/mod.rs: Removed underscore normalization - mod.rs: Updated command list
This commit is contained in:
parent
2e2fc43454
commit
b1193afda2
8 changed files with 499 additions and 117 deletions
|
|
@ -347,11 +347,11 @@ impl BasicCompiler {
|
|||
}
|
||||
// Keywords now use spaces directly in Rhai registration
|
||||
// Only normalize keywords that still need it for special preprocessing
|
||||
// Keywords now use spaces directly in Rhai registration
|
||||
// Only normalize keywords that need underscores for Rhai parsing
|
||||
let normalized = trimmed
|
||||
.replace("FOR EACH", "FOR_EACH")
|
||||
.replace("EXIT FOR", "EXIT_FOR")
|
||||
.replace("GENERATE PDF", "GENERATE_PDF")
|
||||
.replace("MERGE PDF", "MERGE_PDF")
|
||||
.replace("GROUP BY", "GROUP_BY");
|
||||
if normalized.starts_with("SET SCHEDULE") || trimmed.starts_with("SET SCHEDULE") {
|
||||
has_schedule = true;
|
||||
|
|
|
|||
|
|
@ -146,11 +146,15 @@ pub fn register_update_keyword(state: Arc<AppState>, _user: UserSession, engine:
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
/// DELETE "table", filter
|
||||
/// Deletes records from a table matching the filter
|
||||
/// DELETE - Unified delete keyword
|
||||
/// Automatically detects context:
|
||||
/// - DELETE "url" - HTTP DELETE request
|
||||
/// - DELETE "table", "filter" - Database delete with WHERE clause
|
||||
/// - DELETE "file.txt" - File deletion
|
||||
pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
|
||||
let state_clone = Arc::clone(&state);
|
||||
|
||||
// DELETE with two arguments: table + filter (SQL style)
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["DELETE", "$expr$", ",", "$expr$"],
|
||||
|
|
@ -159,16 +163,52 @@ pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine:
|
|||
let first_arg = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||
let second_arg = context.eval_expression_tree(&inputs[1])?.to_string();
|
||||
|
||||
// Detect if this is a table delete or HTTP delete based on first arg
|
||||
// Auto-detect: HTTP URL vs Database table
|
||||
if first_arg.starts_with("http://") || first_arg.starts_with("https://") {
|
||||
// This is an HTTP DELETE - delegate to http_operations
|
||||
trace!("DELETE_HTTP detected, URL: {}", first_arg);
|
||||
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"Use DELETE_HTTP for HTTP DELETE requests".into(),
|
||||
rhai::Position::NONE,
|
||||
)));
|
||||
}
|
||||
// HTTP DELETE with body/params
|
||||
trace!("DELETE HTTP with data: {}", first_arg);
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let url_clone = first_arg.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build();
|
||||
|
||||
let _ = if let Ok(rt) = rt {
|
||||
let result = rt.block_on(async move {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.delete(&url_clone)
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP error: {}", e))?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Response error: {}", e))
|
||||
});
|
||||
tx.send(result)
|
||||
} else {
|
||||
tx.send(Err("Failed to build runtime".to_string()))
|
||||
};
|
||||
});
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
|
||||
Ok(Ok(response)) => Ok(Dynamic::from(response)),
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("DELETE failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"DELETE timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
// Database DELETE with WHERE clause
|
||||
trace!("DELETE from table: {}, filter: {}", first_arg, second_arg);
|
||||
|
||||
let mut conn = state_clone
|
||||
|
|
@ -180,9 +220,81 @@ pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine:
|
|||
.map_err(|e| format!("DELETE error: {}", e))?;
|
||||
|
||||
Ok(Dynamic::from(result))
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// DELETE with single argument: URL or file path
|
||||
let state_clone2 = Arc::clone(&state);
|
||||
engine
|
||||
.register_custom_syntax(&["DELETE", "$expr$"], false, move |context, inputs| {
|
||||
let target = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||
|
||||
// Auto-detect: HTTP URL vs File path
|
||||
if target.starts_with("http://") || target.starts_with("https://") {
|
||||
// HTTP DELETE
|
||||
trace!("DELETE HTTP: {}", target);
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let url_clone = target.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build();
|
||||
|
||||
let _ = if let Ok(rt) = rt {
|
||||
let result = rt.block_on(async move {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.delete(&url_clone)
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP error: {}", e))?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Response error: {}", e))
|
||||
});
|
||||
tx.send(result)
|
||||
} else {
|
||||
tx.send(Err("Failed to build runtime".to_string()))
|
||||
};
|
||||
});
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
|
||||
Ok(Ok(response)) => Ok(Dynamic::from(response)),
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("DELETE failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"DELETE timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
// File deletion
|
||||
trace!("DELETE file: {}", target);
|
||||
|
||||
// Use the file operations delete logic
|
||||
let _state = Arc::clone(&state_clone2);
|
||||
|
||||
// Simple file delete - construct path in .gbdrive
|
||||
let file_path = std::path::Path::new(&target);
|
||||
if file_path.exists() {
|
||||
std::fs::remove_file(file_path)
|
||||
.map_err(|e| format!("File delete error: {}", e))?;
|
||||
Ok(Dynamic::from(true))
|
||||
} else {
|
||||
// Try in .gbdrive
|
||||
Ok(Dynamic::from(format!("File not found: {}", target)))
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// MERGE "table", data, key_field
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod on_error;
|
||||
pub mod throw;
|
||||
|
||||
use crate::shared::models::UserSession;
|
||||
|
|
@ -6,12 +7,21 @@ use log::debug;
|
|||
use rhai::{Dynamic, Engine, EvalAltResult, Map, Position};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Re-export ON ERROR functions for easy access
|
||||
pub use on_error::{
|
||||
clear_last_error, get_error_number, get_last_error, handle_error, handle_string_error,
|
||||
is_error_resume_next_active, set_error_resume_next, set_last_error,
|
||||
};
|
||||
|
||||
pub fn register_error_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||
throw_keyword(&state, user.clone(), engine);
|
||||
error_keyword(&state, user.clone(), engine);
|
||||
is_error_keyword(&state, user.clone(), engine);
|
||||
assert_keyword(&state, user.clone(), engine);
|
||||
log_error_keyword(&state, user, engine);
|
||||
log_error_keyword(&state, user.clone(), engine);
|
||||
|
||||
// Register ON ERROR RESUME NEXT keywords
|
||||
on_error::register_on_error_keywords(state, user, engine);
|
||||
|
||||
debug!("Registered all error handling functions");
|
||||
}
|
||||
|
|
|
|||
307
src/basic/keywords/errors/on_error.rs
Normal file
307
src/basic/keywords/errors/on_error.rs
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
//! ON ERROR RESUME NEXT Implementation
|
||||
//!
|
||||
//! Provides VB-style error handling for BASIC scripts.
|
||||
//! When ON ERROR RESUME NEXT is active, errors are caught and stored
|
||||
//! rather than halting execution.
|
||||
//!
|
||||
//! # Usage
|
||||
//! ```basic
|
||||
//! ON ERROR RESUME NEXT
|
||||
//! result = SOME_RISKY_OPERATION()
|
||||
//! IF ERROR THEN
|
||||
//! TALK "An error occurred: " + ERROR MESSAGE
|
||||
//! END IF
|
||||
//! ON ERROR GOTO 0 ' Disable error handling
|
||||
//! ```
|
||||
|
||||
use crate::shared::models::UserSession;
|
||||
use crate::shared::state::AppState;
|
||||
use log::{debug, trace};
|
||||
use rhai::{Dynamic, Engine, EvalAltResult, Position};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
thread_local! {
|
||||
/// Thread-local flag indicating if ON ERROR RESUME NEXT is active
|
||||
static ERROR_RESUME_NEXT: RefCell<bool> = RefCell::new(false);
|
||||
|
||||
/// Thread-local storage for the last error that occurred
|
||||
static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
|
||||
|
||||
/// Thread-local error number (for compatibility)
|
||||
static ERROR_NUMBER: RefCell<i64> = RefCell::new(0);
|
||||
}
|
||||
|
||||
/// Check if ON ERROR RESUME NEXT is currently active
|
||||
pub fn is_error_resume_next_active() -> bool {
|
||||
ERROR_RESUME_NEXT.with(|flag| *flag.borrow())
|
||||
}
|
||||
|
||||
/// Set the ON ERROR RESUME NEXT state
|
||||
pub fn set_error_resume_next(active: bool) {
|
||||
ERROR_RESUME_NEXT.with(|flag| {
|
||||
*flag.borrow_mut() = active;
|
||||
});
|
||||
if !active {
|
||||
// Clear error state when disabling
|
||||
clear_last_error();
|
||||
}
|
||||
}
|
||||
|
||||
/// Store an error message
|
||||
pub fn set_last_error(message: &str, error_num: i64) {
|
||||
LAST_ERROR.with(|err| {
|
||||
*err.borrow_mut() = Some(message.to_string());
|
||||
});
|
||||
ERROR_NUMBER.with(|num| {
|
||||
*num.borrow_mut() = error_num;
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the last error
|
||||
pub fn clear_last_error() {
|
||||
LAST_ERROR.with(|err| {
|
||||
*err.borrow_mut() = None;
|
||||
});
|
||||
ERROR_NUMBER.with(|num| {
|
||||
*num.borrow_mut() = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the last error message
|
||||
pub fn get_last_error() -> Option<String> {
|
||||
LAST_ERROR.with(|err| err.borrow().clone())
|
||||
}
|
||||
|
||||
/// Get the last error number
|
||||
pub fn get_error_number() -> i64 {
|
||||
ERROR_NUMBER.with(|num| *num.borrow())
|
||||
}
|
||||
|
||||
/// Register ON ERROR keywords with the Rhai engine
|
||||
pub fn register_on_error_keywords(_state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
|
||||
// ON ERROR RESUME NEXT - Enable error trapping
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["ON", "ERROR", "RESUME", "NEXT"],
|
||||
false,
|
||||
move |_context, _inputs| {
|
||||
trace!("ON ERROR RESUME NEXT activated");
|
||||
set_error_resume_next(true);
|
||||
clear_last_error();
|
||||
Ok(Dynamic::UNIT)
|
||||
},
|
||||
)
|
||||
.expect("Failed to register ON ERROR RESUME NEXT");
|
||||
|
||||
// ON ERROR GOTO 0 - Disable error trapping (standard VB syntax)
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["ON", "ERROR", "GOTO", "0"],
|
||||
false,
|
||||
move |_context, _inputs| {
|
||||
trace!("ON ERROR GOTO 0 - Error handling disabled");
|
||||
set_error_resume_next(false);
|
||||
Ok(Dynamic::UNIT)
|
||||
},
|
||||
)
|
||||
.expect("Failed to register ON ERROR GOTO 0");
|
||||
|
||||
// CLEAR ERROR - Clear the current error state
|
||||
engine
|
||||
.register_custom_syntax(&["CLEAR", "ERROR"], false, move |_context, _inputs| {
|
||||
trace!("CLEAR ERROR executed");
|
||||
clear_last_error();
|
||||
Ok(Dynamic::UNIT)
|
||||
})
|
||||
.expect("Failed to register CLEAR ERROR");
|
||||
|
||||
// ERROR - Check if an error occurred (returns true/false)
|
||||
// Used as: IF ERROR THEN ...
|
||||
engine.register_fn("ERROR", || -> bool { get_last_error().is_some() });
|
||||
|
||||
// ERROR MESSAGE - Get the last error message
|
||||
// Used as: msg = ERROR MESSAGE
|
||||
engine
|
||||
.register_custom_syntax(&["ERROR", "MESSAGE"], false, move |_context, _inputs| {
|
||||
let msg = get_last_error().unwrap_or_default();
|
||||
Ok(Dynamic::from(msg))
|
||||
})
|
||||
.expect("Failed to register ERROR MESSAGE");
|
||||
|
||||
// ERR - Get error number (VB compatibility)
|
||||
engine.register_fn("ERR", || -> i64 { get_error_number() });
|
||||
|
||||
// ERR.NUMBER - Alias for ERR
|
||||
engine.register_fn("ERR_NUMBER", || -> i64 { get_error_number() });
|
||||
|
||||
// ERR.DESCRIPTION - Get error description
|
||||
engine.register_fn("ERR_DESCRIPTION", || -> String {
|
||||
get_last_error().unwrap_or_default()
|
||||
});
|
||||
|
||||
// ERR.CLEAR - Clear the error
|
||||
engine.register_fn("ERR_CLEAR", || {
|
||||
clear_last_error();
|
||||
});
|
||||
|
||||
debug!("Registered ON ERROR keywords");
|
||||
}
|
||||
|
||||
/// Wrapper function to execute code with ON ERROR RESUME NEXT support
|
||||
/// This should be called around risky operations
|
||||
pub fn try_execute<F, T>(operation: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce() -> Result<T, Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
match operation() {
|
||||
Ok(result) => {
|
||||
// Clear any previous error on success
|
||||
if is_error_resume_next_active() {
|
||||
clear_last_error();
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
if is_error_resume_next_active() {
|
||||
// Store the error but don't propagate
|
||||
set_last_error(&error_msg, 1);
|
||||
trace!("Error caught by ON ERROR RESUME NEXT: {}", error_msg);
|
||||
Err(error_msg)
|
||||
} else {
|
||||
// No error handling, propagate the error
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper macro to wrap operations with ON ERROR RESUME NEXT support
|
||||
/// Returns Dynamic::UNIT on error if ON ERROR RESUME NEXT is active
|
||||
#[macro_export]
|
||||
macro_rules! with_error_handling {
|
||||
($result:expr) => {
|
||||
match $result {
|
||||
Ok(val) => {
|
||||
$crate::basic::keywords::errors::on_error::clear_last_error();
|
||||
Ok(val)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("{}", e);
|
||||
if $crate::basic::keywords::errors::on_error::is_error_resume_next_active() {
|
||||
$crate::basic::keywords::errors::on_error::set_last_error(&error_msg, 1);
|
||||
Ok(rhai::Dynamic::UNIT)
|
||||
} else {
|
||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
error_msg.into(),
|
||||
rhai::Position::NONE,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a result that respects ON ERROR RESUME NEXT
|
||||
pub fn handle_error<T: Into<Dynamic>>(
|
||||
result: Result<T, Box<dyn std::error::Error + Send + Sync>>,
|
||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match result {
|
||||
Ok(val) => {
|
||||
clear_last_error();
|
||||
Ok(val.into())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
if is_error_resume_next_active() {
|
||||
set_last_error(&error_msg, 1);
|
||||
trace!("Error suppressed by ON ERROR RESUME NEXT: {}", error_msg);
|
||||
Ok(Dynamic::UNIT)
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
error_msg.into(),
|
||||
Position::NONE,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a string error with ON ERROR RESUME NEXT support
|
||||
pub fn handle_string_error(error_msg: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
if is_error_resume_next_active() {
|
||||
set_last_error(error_msg, 1);
|
||||
trace!("Error suppressed by ON ERROR RESUME NEXT: {}", error_msg);
|
||||
Ok(Dynamic::UNIT)
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
error_msg.to_string().into(),
|
||||
Position::NONE,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_resume_next_flag() {
|
||||
// Initially should be false
|
||||
assert!(!is_error_resume_next_active());
|
||||
|
||||
// Enable it
|
||||
set_error_resume_next(true);
|
||||
assert!(is_error_resume_next_active());
|
||||
|
||||
// Disable it
|
||||
set_error_resume_next(false);
|
||||
assert!(!is_error_resume_next_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_storage() {
|
||||
clear_last_error();
|
||||
assert!(get_last_error().is_none());
|
||||
assert_eq!(get_error_number(), 0);
|
||||
|
||||
set_last_error("Test error", 42);
|
||||
assert_eq!(get_last_error(), Some("Test error".to_string()));
|
||||
assert_eq!(get_error_number(), 42);
|
||||
|
||||
clear_last_error();
|
||||
assert!(get_last_error().is_none());
|
||||
assert_eq!(get_error_number(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_error_without_resume_next() {
|
||||
set_error_resume_next(false);
|
||||
clear_last_error();
|
||||
|
||||
let result: Result<i32, Box<dyn std::error::Error + Send + Sync>> =
|
||||
Err("Test error".into());
|
||||
let handled = handle_error(result);
|
||||
|
||||
// Should return error when ON ERROR RESUME NEXT is not active
|
||||
assert!(handled.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_error_with_resume_next() {
|
||||
set_error_resume_next(true);
|
||||
clear_last_error();
|
||||
|
||||
let result: Result<i32, Box<dyn std::error::Error + Send + Sync>> =
|
||||
Err("Test error".into());
|
||||
let handled = handle_error(result);
|
||||
|
||||
// Should return Ok(UNIT) when ON ERROR RESUME NEXT is active
|
||||
assert!(handled.is_ok());
|
||||
assert_eq!(get_last_error(), Some("Test error".to_string()));
|
||||
|
||||
// Cleanup
|
||||
set_error_resume_next(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -812,20 +812,23 @@ pub fn register_download_keyword(state: Arc<AppState>, user: UserSession, engine
|
|||
|
||||
/// GENERATE_PDF template, data, "output.pdf"
|
||||
/// Generates a PDF from a template with data
|
||||
/// GENERATE PDF template, data, "output.pdf"
|
||||
/// Generates a PDF from a template with data
|
||||
pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||
let state_clone = Arc::clone(&state);
|
||||
let user_clone = user.clone();
|
||||
|
||||
// GENERATE PDF template, data, output
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["GENERATE_PDF", "$expr$", ",", "$expr$", ",", "$expr$"],
|
||||
&["GENERATE", "PDF", "$expr$", ",", "$expr$", ",", "$expr$"],
|
||||
false,
|
||||
move |context, inputs| {
|
||||
let template = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||
let data = context.eval_expression_tree(&inputs[1])?;
|
||||
let output = context.eval_expression_tree(&inputs[2])?.to_string();
|
||||
|
||||
trace!("GENERATE_PDF template: {}, output: {}", template, output);
|
||||
trace!("GENERATE PDF template: {}, output: {}", template, output);
|
||||
|
||||
let state_for_task = Arc::clone(&state_clone);
|
||||
let user_for_task = user_clone.clone();
|
||||
|
|
@ -858,7 +861,7 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
|
|||
};
|
||||
|
||||
if send_err.is_some() {
|
||||
error!("Failed to send GENERATE_PDF result from thread");
|
||||
error!("Failed to send GENERATE PDF result from thread");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -870,17 +873,17 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
|
|||
Ok(Dynamic::from(map))
|
||||
}
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("GENERATE_PDF failed: {}", e).into(),
|
||||
format!("GENERATE PDF failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"GENERATE_PDF timed out".into(),
|
||||
"GENERATE PDF timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
)))
|
||||
}
|
||||
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("GENERATE_PDF thread failed: {}", e).into(),
|
||||
format!("GENERATE PDF thread failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
|
|
@ -889,21 +892,22 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
/// MERGE_PDF files, "merged.pdf"
|
||||
/// MERGE PDF files, "merged.pdf"
|
||||
/// Merges multiple PDF files into one
|
||||
pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||
let state_clone = Arc::clone(&state);
|
||||
let user_clone = user.clone();
|
||||
|
||||
// MERGE PDF files, output
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["MERGE_PDF", "$expr$", ",", "$expr$"],
|
||||
&["MERGE", "PDF", "$expr$", ",", "$expr$"],
|
||||
false,
|
||||
move |context, inputs| {
|
||||
let files = context.eval_expression_tree(&inputs[0])?;
|
||||
let output = context.eval_expression_tree(&inputs[1])?.to_string();
|
||||
|
||||
trace!("MERGE_PDF to: {}", output);
|
||||
trace!("MERGE PDF to: {}", output);
|
||||
|
||||
let state_for_task = Arc::clone(&state_clone);
|
||||
let user_for_task = user_clone.clone();
|
||||
|
|
@ -946,7 +950,7 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
|
|||
};
|
||||
|
||||
if send_err.is_some() {
|
||||
error!("Failed to send MERGE_PDF result from thread");
|
||||
error!("Failed to send MERGE PDF result from thread");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -958,17 +962,17 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
|
|||
Ok(Dynamic::from(map))
|
||||
}
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("MERGE_PDF failed: {}", e).into(),
|
||||
format!("MERGE PDF failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"MERGE_PDF timed out".into(),
|
||||
"MERGE PDF timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
)))
|
||||
}
|
||||
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("MERGE_PDF thread failed: {}", e).into(),
|
||||
format!("MERGE PDF thread failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,11 +244,15 @@ pub fn register_patch_keyword(state: Arc<AppState>, _user: UserSession, engine:
|
|||
|
||||
/// DELETE "url"
|
||||
/// Sends an HTTP DELETE request
|
||||
pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
|
||||
let _state_clone = Arc::clone(&state);
|
||||
|
||||
// DELETE HTTP (space-separated - preferred)
|
||||
let _state_clone2 = Arc::clone(&state);
|
||||
/// DELETE HTTP "url" - Backwards compatibility alias
|
||||
/// Note: Prefer using just DELETE "url" which auto-detects HTTP URLs
|
||||
pub fn register_delete_http_keyword(
|
||||
_state: Arc<AppState>,
|
||||
_user: UserSession,
|
||||
engine: &mut Engine,
|
||||
) {
|
||||
// DELETE HTTP "url" - kept for backwards compatibility
|
||||
// The unified DELETE in data_operations.rs handles this automatically now
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["DELETE", "HTTP", "$expr$"],
|
||||
|
|
@ -301,60 +305,6 @@ pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, en
|
|||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// DELETE HTTP (spaces - preferred syntax)
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&["DELETE", "HTTP", "$expr$"],
|
||||
false,
|
||||
move |context, inputs| {
|
||||
let url = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||
|
||||
trace!("DELETE HTTP request to: {}", url);
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let url_clone = url.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build();
|
||||
|
||||
let send_err = if let Ok(rt) = rt {
|
||||
let result = rt.block_on(async move {
|
||||
execute_http_request(Method::DELETE, &url_clone, None, None).await
|
||||
});
|
||||
tx.send(result).err()
|
||||
} else {
|
||||
tx.send(Err("Failed to build tokio runtime".into())).err()
|
||||
};
|
||||
|
||||
if send_err.is_some() {
|
||||
error!("Failed to send DELETE HTTP result from thread");
|
||||
}
|
||||
});
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
|
||||
Ok(Ok(response)) => Ok(json_to_dynamic(&response)),
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("DELETE HTTP failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"DELETE HTTP request timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
)))
|
||||
}
|
||||
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("DELETE HTTP thread failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// SET HEADER "name", "value"
|
||||
|
|
|
|||
|
|
@ -12,17 +12,11 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
|
|||
let state_clone = Arc::clone(&state);
|
||||
let user_clone = user.clone();
|
||||
|
||||
// SEND MAIL to, subject, body, attachments
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
&[
|
||||
"SEND_MAIL",
|
||||
"$expr$",
|
||||
",",
|
||||
"$expr$",
|
||||
",",
|
||||
"$expr$",
|
||||
",",
|
||||
"$expr$",
|
||||
"SEND", "MAIL", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$",
|
||||
],
|
||||
false,
|
||||
move |context, inputs| {
|
||||
|
|
@ -43,7 +37,7 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
|
|||
}
|
||||
|
||||
trace!(
|
||||
"SEND_MAIL: to={}, subject={}, attachments={:?} for user={}",
|
||||
"SEND MAIL: to={}, subject={}, attachments={:?} for user={}",
|
||||
to,
|
||||
subject,
|
||||
attachments,
|
||||
|
|
@ -80,24 +74,24 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
|
|||
};
|
||||
|
||||
if send_err.is_some() {
|
||||
error!("Failed to send SEND_MAIL result from thread");
|
||||
error!("Failed to send SEND MAIL result from thread");
|
||||
}
|
||||
});
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
||||
Ok(Ok(message_id)) => Ok(Dynamic::from(message_id)),
|
||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("SEND_MAIL failed: {}", e).into(),
|
||||
format!("SEND MAIL failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
"SEND_MAIL timed out".into(),
|
||||
"SEND MAIL timed out".into(),
|
||||
rhai::Position::NONE,
|
||||
)))
|
||||
}
|
||||
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||
format!("SEND_MAIL thread failed: {}", e).into(),
|
||||
format!("SEND MAIL thread failed: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))),
|
||||
}
|
||||
|
|
@ -105,7 +99,7 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// Register SEND_TEMPLATE for bulk templated emails
|
||||
// Register SEND TEMPLATE for bulk templated emails
|
||||
let state_clone2 = Arc::clone(&state);
|
||||
let user_clone2 = user.clone();
|
||||
|
||||
|
|
|
|||
|
|
@ -325,9 +325,9 @@ impl ScriptService {
|
|||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE_HTTP",
|
||||
"SET_HEADER",
|
||||
"CLEAR_HEADERS",
|
||||
"DELETE",
|
||||
"SET HEADER",
|
||||
"CLEAR HEADERS",
|
||||
"GRAPHQL",
|
||||
"SOAP",
|
||||
// Data Operations
|
||||
|
|
@ -342,11 +342,10 @@ impl ScriptService {
|
|||
"AGGREGATE",
|
||||
"JOIN",
|
||||
"PIVOT",
|
||||
"GROUP_BY",
|
||||
"GROUP BY",
|
||||
// File Operations
|
||||
"READ",
|
||||
"WRITE",
|
||||
"DELETE_FILE",
|
||||
"COPY",
|
||||
"MOVE",
|
||||
"LIST",
|
||||
|
|
@ -354,8 +353,8 @@ impl ScriptService {
|
|||
"EXTRACT",
|
||||
"UPLOAD",
|
||||
"DOWNLOAD",
|
||||
"GENERATE_PDF",
|
||||
"MERGE_PDF",
|
||||
"GENERATE PDF",
|
||||
"MERGE PDF",
|
||||
// Webhook
|
||||
"WEBHOOK",
|
||||
// Social Media
|
||||
|
|
@ -370,9 +369,15 @@ impl ScriptService {
|
|||
"GET TWITTER METRICS",
|
||||
"DELETE POST",
|
||||
// Template & Messaging
|
||||
"SEND MAIL",
|
||||
"SEND TEMPLATE",
|
||||
"CREATE TEMPLATE",
|
||||
"GET TEMPLATE",
|
||||
// Error Handling
|
||||
"ON ERROR RESUME NEXT",
|
||||
"ON ERROR GOTO",
|
||||
"CLEAR ERROR",
|
||||
"ERROR MESSAGE",
|
||||
// Form Handling
|
||||
"ON FORM SUBMIT",
|
||||
// Lead Scoring
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue