mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 10:08:29 +02:00
Add yaak-actions-builtin crate and integrate with CLI
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -8005,6 +8005,22 @@ dependencies = [
|
|||||||
"ts-rs",
|
"ts-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-actions-builtin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"yaak-actions",
|
||||||
|
"yaak-crypto",
|
||||||
|
"yaak-http",
|
||||||
|
"yaak-models",
|
||||||
|
"yaak-plugins",
|
||||||
|
"yaak-templates",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-app"
|
name = "yaak-app"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@@ -8074,6 +8090,8 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"yaak-actions",
|
||||||
|
"yaak-actions-builtin",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-actions",
|
"crates/yaak-actions",
|
||||||
|
"crates/yaak-actions-builtin",
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
"crates/yaak-crypto",
|
"crates/yaak-crypto",
|
||||||
@@ -47,6 +48,7 @@ ts-rs = "11.1.0"
|
|||||||
|
|
||||||
# Internal crates - shared
|
# Internal crates - shared
|
||||||
yaak-actions = { path = "crates/yaak-actions" }
|
yaak-actions = { path = "crates/yaak-actions" }
|
||||||
|
yaak-actions-builtin = { path = "crates/yaak-actions-builtin" }
|
||||||
yaak-core = { path = "crates/yaak-core" }
|
yaak-core = { path = "crates/yaak-core" }
|
||||||
yaak-common = { path = "crates/yaak-common" }
|
yaak-common = { path = "crates/yaak-common" }
|
||||||
yaak-crypto = { path = "crates/yaak-crypto" }
|
yaak-crypto = { path = "crates/yaak-crypto" }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ env_logger = "0.11"
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
|
yaak-actions = { workspace = true }
|
||||||
|
yaak-actions-builtin = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use log::info;
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
|
||||||
use yaak_http::sender::{HttpSender, ReqwestSender};
|
use yaak_http::sender::{HttpSender, ReqwestSender};
|
||||||
use yaak_http::types::{SendableHttpRequest, SendableHttpRequestOptions};
|
use yaak_http::types::{SendableHttpRequest, SendableHttpRequestOptions};
|
||||||
use yaak_models::models::{HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
use yaak_models::models::HttpRequest;
|
||||||
use yaak_models::render::make_vars_hashmap;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
use yaak_plugins::events::PluginContext;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
|
||||||
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaakcli")]
|
#[command(name = "yaakcli")]
|
||||||
@@ -72,86 +64,6 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render an HTTP request with template variables and plugin functions
|
|
||||||
async fn render_http_request(
|
|
||||||
r: &HttpRequest,
|
|
||||||
environment_chain: Vec<yaak_models::models::Environment>,
|
|
||||||
cb: &PluginTemplateCallback,
|
|
||||||
opt: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<HttpRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut url_parameters = Vec::new();
|
|
||||||
for p in r.url_parameters.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
url_parameters.push(HttpUrlParameter {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut headers = Vec::new();
|
|
||||||
for p in r.headers.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
headers.push(HttpRequestHeader {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
for (k, v) in r.body.clone() {
|
|
||||||
body.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
match r.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(tmpl)) => {
|
|
||||||
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
info!(
|
|
||||||
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (k, v) in r.authentication.clone() {
|
|
||||||
if k == "disabled" {
|
|
||||||
auth.insert(k, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(r.url.clone().as_str(), vars, cb, opt).await?;
|
|
||||||
|
|
||||||
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
|
||||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
|
||||||
|
|
||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -176,10 +88,6 @@ async fn main() {
|
|||||||
|
|
||||||
let db = query_manager.connect();
|
let db = query_manager.connect();
|
||||||
|
|
||||||
// Initialize encryption manager for secure() template function
|
|
||||||
// Use the same app_id as the Tauri app for keyring access
|
|
||||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
|
||||||
|
|
||||||
// Initialize plugin manager for template functions
|
// Initialize plugin manager for template functions
|
||||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||||
let installed_plugin_dir = data_dir.join("installed-plugins");
|
let installed_plugin_dir = data_dir.join("installed-plugins");
|
||||||
@@ -198,9 +106,9 @@ async fn main() {
|
|||||||
// Create plugin manager (plugins may not be available in CLI context)
|
// Create plugin manager (plugins may not be available in CLI context)
|
||||||
let plugin_manager = Arc::new(
|
let plugin_manager = Arc::new(
|
||||||
PluginManager::new(
|
PluginManager::new(
|
||||||
vendored_plugin_dir,
|
vendored_plugin_dir.clone(),
|
||||||
installed_plugin_dir,
|
installed_plugin_dir.clone(),
|
||||||
node_bin_path,
|
node_bin_path.clone(),
|
||||||
plugin_runtime_main,
|
plugin_runtime_main,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -239,94 +147,67 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Send { request_id } => {
|
Commands::Send { request_id } => {
|
||||||
let request = db.get_http_request(&request_id).expect("Failed to get request");
|
use yaak_actions::{
|
||||||
|
ActionExecutor, ActionId, ActionParams, ActionResult, ActionTarget, CurrentContext,
|
||||||
|
};
|
||||||
|
use yaak_actions_builtin::{BuiltinActionDependencies, register_http_actions};
|
||||||
|
|
||||||
// Resolve environment chain for variable substitution
|
// Create dependencies
|
||||||
let environment_chain = db
|
let deps = BuiltinActionDependencies::new_standalone(
|
||||||
.resolve_environments(
|
&db_path,
|
||||||
&request.workspace_id,
|
&blob_path,
|
||||||
request.folder_id.as_deref(),
|
&app_id,
|
||||||
cli.environment.as_deref(),
|
vendored_plugin_dir.clone(),
|
||||||
)
|
installed_plugin_dir.clone(),
|
||||||
.unwrap_or_default();
|
node_bin_path.clone(),
|
||||||
|
|
||||||
// Create template callback with plugin support
|
|
||||||
let plugin_context = PluginContext::new(None, Some(request.workspace_id.clone()));
|
|
||||||
let template_callback = PluginTemplateCallback::new(
|
|
||||||
plugin_manager.clone(),
|
|
||||||
encryption_manager.clone(),
|
|
||||||
&plugin_context,
|
|
||||||
RenderPurpose::Send,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Render templates in the request
|
|
||||||
let rendered_request = render_http_request(
|
|
||||||
&request,
|
|
||||||
environment_chain,
|
|
||||||
&template_callback,
|
|
||||||
&RenderOptions::throw(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to render request templates");
|
.expect("Failed to initialize dependencies");
|
||||||
|
|
||||||
if cli.verbose {
|
// Create executor and register actions
|
||||||
println!("> {} {}", rendered_request.method, rendered_request.url);
|
let executor = ActionExecutor::new();
|
||||||
}
|
executor.register_builtin_groups().await.expect("Failed to register groups");
|
||||||
|
register_http_actions(&executor, &deps).await.expect("Failed to register HTTP actions");
|
||||||
|
|
||||||
// Convert to sendable request
|
// Prepare context
|
||||||
let sendable = SendableHttpRequest::from_http_request(
|
let context = CurrentContext {
|
||||||
&rendered_request,
|
target: Some(ActionTarget::HttpRequest { id: request_id.clone() }),
|
||||||
SendableHttpRequestOptions::default(),
|
environment_id: cli.environment.clone(),
|
||||||
)
|
workspace_id: None,
|
||||||
.await
|
has_window: false,
|
||||||
.expect("Failed to build request");
|
can_prompt: false,
|
||||||
|
|
||||||
// Create event channel for progress
|
|
||||||
let (event_tx, mut event_rx) = mpsc::channel(100);
|
|
||||||
|
|
||||||
// Spawn task to print events if verbose
|
|
||||||
let verbose = cli.verbose;
|
|
||||||
let verbose_handle = if verbose {
|
|
||||||
Some(tokio::spawn(async move {
|
|
||||||
while let Some(event) = event_rx.recv().await {
|
|
||||||
println!("{}", event);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
// Drain events silently
|
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
// Prepare params
|
||||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
let params = ActionParams {
|
||||||
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
data: serde_json::json!({
|
||||||
|
"render": true,
|
||||||
|
"follow_redirects": false,
|
||||||
|
"timeout_ms": 30000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// Wait for event handler to finish
|
// Invoke action
|
||||||
if let Some(handle) = verbose_handle {
|
let action_id = ActionId::builtin("http", "send-request");
|
||||||
let _ = handle.await;
|
let result = executor.invoke(&action_id, context, params).await.expect("Action failed");
|
||||||
}
|
|
||||||
|
|
||||||
// Print response
|
// Handle result
|
||||||
if verbose {
|
match result {
|
||||||
println!();
|
ActionResult::Success { data, message } => {
|
||||||
}
|
if let Some(msg) = message {
|
||||||
println!(
|
println!("{}", msg);
|
||||||
"HTTP {} {}",
|
}
|
||||||
response.status,
|
if let Some(data) = data {
|
||||||
response.status_reason.as_deref().unwrap_or("")
|
println!("{}", serde_json::to_string_pretty(&data).unwrap());
|
||||||
);
|
}
|
||||||
|
}
|
||||||
if verbose {
|
ActionResult::RequiresInput { .. } => {
|
||||||
for (name, value) in &response.headers {
|
eprintln!("Action requires input (not supported in CLI)");
|
||||||
println!("{}: {}", name, value);
|
}
|
||||||
|
ActionResult::Cancelled => {
|
||||||
|
eprintln!("Action cancelled");
|
||||||
}
|
}
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print body
|
|
||||||
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
|
||||||
println!("{}", body);
|
|
||||||
}
|
}
|
||||||
Commands::Get { url } => {
|
Commands::Get { url } => {
|
||||||
if cli.verbose {
|
if cli.verbose {
|
||||||
|
|||||||
18
crates/yaak-actions-builtin/Cargo.toml
Normal file
18
crates/yaak-actions-builtin/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak-actions-builtin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Gregory Schier"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
yaak-actions = { workspace = true }
|
||||||
|
yaak-http = { workspace = true }
|
||||||
|
yaak-models = { workspace = true }
|
||||||
|
yaak-templates = { workspace = true }
|
||||||
|
yaak-plugins = { workspace = true }
|
||||||
|
yaak-crypto = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["sync", "rt-multi-thread"] }
|
||||||
|
log = { workspace = true }
|
||||||
88
crates/yaak-actions-builtin/src/dependencies.rs
Normal file
88
crates/yaak-actions-builtin/src/dependencies.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//! Dependency injection for built-in actions.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
|
use yaak_models::query_manager::QueryManager;
|
||||||
|
use yaak_plugins::events::PluginContext;
|
||||||
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
/// Dependencies needed by built-in action implementations.
|
||||||
|
///
|
||||||
|
/// This struct bundles all the dependencies that action handlers need,
|
||||||
|
/// providing a clean way to initialize them in different contexts
|
||||||
|
/// (CLI, Tauri app, MCP server, etc.).
|
||||||
|
pub struct BuiltinActionDependencies {
|
||||||
|
pub query_manager: Arc<QueryManager>,
|
||||||
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuiltinActionDependencies {
|
||||||
|
/// Create dependencies for standalone usage (CLI, MCP server, etc.)
|
||||||
|
///
|
||||||
|
/// This initializes all the necessary managers following the same pattern
|
||||||
|
/// as the yaak-cli implementation.
|
||||||
|
pub async fn new_standalone(
|
||||||
|
db_path: &Path,
|
||||||
|
blob_path: &Path,
|
||||||
|
app_id: &str,
|
||||||
|
plugin_vendored_dir: PathBuf,
|
||||||
|
plugin_installed_dir: PathBuf,
|
||||||
|
node_path: PathBuf,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
// Initialize database
|
||||||
|
let (query_manager, _, _) = yaak_models::init_standalone(db_path, blob_path)?;
|
||||||
|
|
||||||
|
// Initialize encryption manager (takes QueryManager by value)
|
||||||
|
let encryption_manager = Arc::new(EncryptionManager::new(
|
||||||
|
query_manager.clone(),
|
||||||
|
app_id.to_string(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let query_manager = Arc::new(query_manager);
|
||||||
|
|
||||||
|
// Find plugin runtime
|
||||||
|
let plugin_runtime_main = std::env::var("YAAK_PLUGIN_RUNTIME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
// Development fallback
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize plugin manager
|
||||||
|
let plugin_manager = Arc::new(
|
||||||
|
PluginManager::new(
|
||||||
|
plugin_vendored_dir,
|
||||||
|
plugin_installed_dir,
|
||||||
|
node_path,
|
||||||
|
plugin_runtime_main,
|
||||||
|
false, // not sandboxed in CLI
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize plugins from database
|
||||||
|
let db = query_manager.connect();
|
||||||
|
let plugins = db.list_plugins().unwrap_or_default();
|
||||||
|
if !plugins.is_empty() {
|
||||||
|
let errors = plugin_manager
|
||||||
|
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
||||||
|
.await;
|
||||||
|
for (plugin_dir, error_msg) in errors {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to initialize plugin '{}': {}",
|
||||||
|
plugin_dir,
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
query_manager,
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/yaak-actions-builtin/src/http/mod.rs
Normal file
24
crates/yaak-actions-builtin/src/http/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! HTTP action implementations.
|
||||||
|
|
||||||
|
pub mod send;
|
||||||
|
|
||||||
|
use crate::BuiltinActionDependencies;
|
||||||
|
use yaak_actions::{ActionError, ActionExecutor, ActionSource};
|
||||||
|
|
||||||
|
/// Register all HTTP-related actions with the executor.
|
||||||
|
pub async fn register_http_actions(
|
||||||
|
executor: &ActionExecutor,
|
||||||
|
deps: &BuiltinActionDependencies,
|
||||||
|
) -> Result<(), ActionError> {
|
||||||
|
let handler = send::HttpSendActionHandler {
|
||||||
|
query_manager: deps.query_manager.clone(),
|
||||||
|
plugin_manager: deps.plugin_manager.clone(),
|
||||||
|
encryption_manager: deps.encryption_manager.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
executor
|
||||||
|
.register(send::metadata(), ActionSource::Builtin, handler)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
293
crates/yaak-actions-builtin/src/http/send.rs
Normal file
293
crates/yaak-actions-builtin/src/http/send.rs
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
//! HTTP send action implementation.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use yaak_actions::{
|
||||||
|
ActionError, ActionGroupId, ActionHandler, ActionId, ActionMetadata,
|
||||||
|
ActionParams, ActionResult, ActionScope, CurrentContext,
|
||||||
|
RequiredContext,
|
||||||
|
};
|
||||||
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
|
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||||
|
use yaak_http::sender::{HttpSender, ReqwestSender};
|
||||||
|
use yaak_http::types::{SendableHttpRequest, SendableHttpRequestOptions};
|
||||||
|
use yaak_models::models::{HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||||
|
use yaak_models::query_manager::QueryManager;
|
||||||
|
use yaak_models::render::make_vars_hashmap;
|
||||||
|
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
||||||
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
|
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions};
|
||||||
|
|
||||||
|
/// Handler for HTTP send action.
|
||||||
|
pub struct HttpSendActionHandler {
|
||||||
|
pub query_manager: Arc<QueryManager>,
|
||||||
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata for the HTTP send action.
|
||||||
|
pub fn metadata() -> ActionMetadata {
|
||||||
|
ActionMetadata {
|
||||||
|
id: ActionId::builtin("http", "send-request"),
|
||||||
|
label: "Send HTTP Request".to_string(),
|
||||||
|
description: Some("Execute an HTTP request and return the response".to_string()),
|
||||||
|
icon: Some("play".to_string()),
|
||||||
|
scope: ActionScope::HttpRequest,
|
||||||
|
keyboard_shortcut: None,
|
||||||
|
requires_selection: true,
|
||||||
|
enabled_condition: None,
|
||||||
|
group_id: Some(ActionGroupId::builtin("send")),
|
||||||
|
order: 10,
|
||||||
|
required_context: RequiredContext::requires_target(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionHandler for HttpSendActionHandler {
|
||||||
|
fn handle(
|
||||||
|
&self,
|
||||||
|
context: CurrentContext,
|
||||||
|
params: ActionParams,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn std::future::Future<Output = Result<ActionResult, ActionError>> + Send + 'static>,
|
||||||
|
> {
|
||||||
|
let query_manager = self.query_manager.clone();
|
||||||
|
let plugin_manager = self.plugin_manager.clone();
|
||||||
|
let encryption_manager = self.encryption_manager.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
// Extract request_id from context
|
||||||
|
let request_id = context
|
||||||
|
.target
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ActionError::ContextMissing {
|
||||||
|
missing_fields: vec!["target".to_string()],
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.id()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ActionError::ContextMissing {
|
||||||
|
missing_fields: vec!["target.id".to_string()],
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Fetch request and environment from database (synchronous)
|
||||||
|
let (request, environment_chain) = {
|
||||||
|
let db = query_manager.connect();
|
||||||
|
|
||||||
|
// Fetch HTTP request from database
|
||||||
|
let request = db.get_http_request(&request_id).map_err(|e| {
|
||||||
|
ActionError::Internal(format!("Failed to fetch request {}: {}", request_id, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Resolve environment chain for variable substitution
|
||||||
|
let environment_chain = if let Some(env_id) = &context.environment_id {
|
||||||
|
db.resolve_environments(
|
||||||
|
&request.workspace_id,
|
||||||
|
request.folder_id.as_deref(),
|
||||||
|
Some(env_id),
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
db.resolve_environments(
|
||||||
|
&request.workspace_id,
|
||||||
|
request.folder_id.as_deref(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
(request, environment_chain)
|
||||||
|
}; // db is dropped here
|
||||||
|
|
||||||
|
// Create template callback with plugin support
|
||||||
|
let plugin_context = PluginContext::new(None, Some(request.workspace_id.clone()));
|
||||||
|
let template_callback = PluginTemplateCallback::new(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
RenderPurpose::Send,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render templates in the request
|
||||||
|
let rendered_request = render_http_request(
|
||||||
|
&request,
|
||||||
|
environment_chain,
|
||||||
|
&template_callback,
|
||||||
|
&RenderOptions::throw(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ActionError::Internal(format!("Failed to render request: {}", e)))?;
|
||||||
|
|
||||||
|
// Build sendable request
|
||||||
|
let options = SendableHttpRequestOptions {
|
||||||
|
timeout: params
|
||||||
|
.data
|
||||||
|
.get("timeout_ms")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|ms| std::time::Duration::from_millis(ms)),
|
||||||
|
follow_redirects: params
|
||||||
|
.data
|
||||||
|
.get("follow_redirects")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sendable = SendableHttpRequest::from_http_request(&rendered_request, options)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ActionError::Internal(format!("Failed to build request: {}", e)))?;
|
||||||
|
|
||||||
|
// Create event channel
|
||||||
|
let (event_tx, mut event_rx) = mpsc::channel(100);
|
||||||
|
|
||||||
|
// Spawn task to drain events
|
||||||
|
let _event_handle = tokio::spawn(async move {
|
||||||
|
while event_rx.recv().await.is_some() {
|
||||||
|
// For now, just drain events
|
||||||
|
// In the future, we could log them or emit them to UI
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
let sender = ReqwestSender::new()
|
||||||
|
.map_err(|e| ActionError::Internal(format!("Failed to create HTTP client: {}", e)))?;
|
||||||
|
let response = sender
|
||||||
|
.send(sendable, event_tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ActionError::Internal(format!("Failed to send request: {}", e)))?;
|
||||||
|
|
||||||
|
// Consume response body
|
||||||
|
let status = response.status;
|
||||||
|
let status_reason = response.status_reason.clone();
|
||||||
|
let headers = response.headers.clone();
|
||||||
|
let url = response.url.clone();
|
||||||
|
|
||||||
|
let (body_text, stats) = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ActionError::Internal(format!("Failed to read response body: {}", e)))?;
|
||||||
|
|
||||||
|
// Return success result with response data
|
||||||
|
Ok(ActionResult::Success {
|
||||||
|
data: Some(json!({
|
||||||
|
"status": status,
|
||||||
|
"statusReason": status_reason,
|
||||||
|
"headers": headers,
|
||||||
|
"body": body_text,
|
||||||
|
"contentLength": stats.size_decompressed,
|
||||||
|
"url": url,
|
||||||
|
})),
|
||||||
|
message: Some(format!("HTTP {}", status)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to render templates in an HTTP request.
|
||||||
|
/// Copied from yaak-cli implementation.
|
||||||
|
async fn render_http_request(
|
||||||
|
r: &HttpRequest,
|
||||||
|
environment_chain: Vec<yaak_models::models::Environment>,
|
||||||
|
cb: &PluginTemplateCallback,
|
||||||
|
opt: &RenderOptions,
|
||||||
|
) -> Result<HttpRequest, String> {
|
||||||
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
|
let mut url_parameters = Vec::new();
|
||||||
|
for p in r.url_parameters.clone() {
|
||||||
|
if !p.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
url_parameters.push(HttpUrlParameter {
|
||||||
|
enabled: p.enabled,
|
||||||
|
name: parse_and_render(p.name.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
value: parse_and_render(p.value.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
id: p.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headers = Vec::new();
|
||||||
|
for p in r.headers.clone() {
|
||||||
|
if !p.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
headers.push(HttpRequestHeader {
|
||||||
|
enabled: p.enabled,
|
||||||
|
name: parse_and_render(p.name.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
value: parse_and_render(p.value.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
id: p.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = BTreeMap::new();
|
||||||
|
for (k, v) in r.body.clone() {
|
||||||
|
body.insert(
|
||||||
|
k,
|
||||||
|
render_json_value_raw(v, vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let authentication = {
|
||||||
|
let mut disabled = false;
|
||||||
|
let mut auth = BTreeMap::new();
|
||||||
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(
|
||||||
|
k,
|
||||||
|
render_json_value_raw(v, vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = parse_and_render(r.url.clone().as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
||||||
|
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
||||||
|
|
||||||
|
Ok(HttpRequest {
|
||||||
|
url,
|
||||||
|
url_parameters,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
authentication,
|
||||||
|
..r.to_owned()
|
||||||
|
})
|
||||||
|
}
|
||||||
11
crates/yaak-actions-builtin/src/lib.rs
Normal file
11
crates/yaak-actions-builtin/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! Built-in action implementations for Yaak.
|
||||||
|
//!
|
||||||
|
//! This crate provides concrete implementations of built-in actions using
|
||||||
|
//! the yaak-actions framework. It depends on domain-specific crates like
|
||||||
|
//! yaak-http, yaak-models, yaak-plugins, etc.
|
||||||
|
|
||||||
|
pub mod dependencies;
|
||||||
|
pub mod http;
|
||||||
|
|
||||||
|
pub use dependencies::BuiltinActionDependencies;
|
||||||
|
pub use http::register_http_actions;
|
||||||
Reference in New Issue
Block a user