Url parameters done

This commit is contained in:
Gregory Schier
2023-11-13 10:52:11 -08:00
parent d289f1fd13
commit df83a61d6f
13 changed files with 308 additions and 238 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n url_parameters,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n ", "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -44,38 +44,43 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "method", "name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "body!: Json<HashMap<String, JsonValue>>", "name": "method",
"ordinal": 9, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "body_type", "name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "authentication!: Json<HashMap<String, JsonValue>>", "name": "body_type",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "authentication_type", "name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "sort_priority", "name": "authentication_type",
"ordinal": 13, "ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 14,
"type_info": "Float" "type_info": "Float"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 14, "ordinal": 15,
"type_info": "Text" "type_info": "Text"
} }
], ],
@@ -93,6 +98,7 @@
false, false,
false, false,
false, false,
false,
true, true,
false, false,
true, true,
@@ -100,5 +106,5 @@
false false
] ]
}, },
"hash": "55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952" "hash": "6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ", "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -44,38 +44,43 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "method", "name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "body!: Json<HashMap<String, JsonValue>>", "name": "method",
"ordinal": 9, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "body_type", "name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "authentication!: Json<HashMap<String, JsonValue>>", "name": "body_type",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "authentication_type", "name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "sort_priority", "name": "authentication_type",
"ordinal": 13, "ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 14,
"type_info": "Float" "type_info": "Float"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 14, "ordinal": 15,
"type_info": "Text" "type_info": "Text"
} }
], ],
@@ -93,6 +98,7 @@
false, false,
false, false,
false, false,
false,
true, true,
false, false,
true, true,
@@ -100,5 +106,5 @@
false false
] ]
}, },
"hash": "ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d" "hash": "7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39"
} }

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759"
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE http_requests
ADD COLUMN url_parameters TEXT NOT NULL DEFAULT '[]';

View File

@@ -10,16 +10,11 @@ extern crate objc;
use std::collections::HashMap; use std::collections::HashMap;
use std::env::current_dir; use std::env::current_dir;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::Write;
use std::process::exit; use std::process::exit;
use base64::Engine;
use fern::colors::ColoredLevelConfig; use fern::colors::ColoredLevelConfig;
use http::{HeaderMap, HeaderValue, Method}; use log::{debug, error, info};
use http::header::{ACCEPT, HeaderName, USER_AGENT};
use log::{debug, error, info, warn};
use rand::random; use rand::random;
use reqwest::redirect::Policy;
use serde::Serialize; use serde::Serialize;
use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
@@ -36,6 +31,7 @@ use window_ext::TrafficLightWindowExt;
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event}; use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
use crate::plugin::{ImportResources, ImportResult}; use crate::plugin::{ImportResources, ImportResult};
use crate::send::actually_send_request;
use crate::updates::YaakUpdater; use crate::updates::YaakUpdater;
mod analytics; mod analytics;
@@ -45,6 +41,7 @@ mod render;
mod window_ext; mod window_ext;
mod window_menu; mod window_menu;
mod updates; mod updates;
mod send;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct CustomResponse { pub struct CustomResponse {
@@ -88,188 +85,6 @@ async fn send_ephemeral_request(
actually_send_request(request, &response, &environment_id2, &app_handle, pool).await actually_send_request(request, &response, &environment_id2, &app_handle, pool).await
} }
async fn actually_send_request(
request: models::HttpRequest,
response: &models::HttpResponse,
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
.expect("Failed to get Workspace");
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
for h in request.headers.0 {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header value: {}", e);
continue;
}
};
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" {
let raw_username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url_string.to_string()).headers(headers);
if let Some(t) = &request.body_type {
let empty_value = &serde_json::to_value("").unwrap();
let b = request.body.0;
if b.contains_key("text") {
let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
request_builder = request_builder.body(body);
} else {
warn!("Unsupported body type: {}", t);
}
}
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
}
};
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = Json(
v.headers()
.iter()
.map(|(k, v)| models::HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap().to_string(),
})
.collect(),
);
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
{
// Write body to FS
let dir = app_handle.path_resolver().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
}
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
}
}
#[tauri::command] #[tauri::command]
async fn import_data( async fn import_data(
window: Window<Wry>, window: Window<Wry>,
@@ -925,8 +740,9 @@ fn main() {
None, None,
); );
} }
RunEvent::WindowEvent { label, event: WindowEvent::Focused(true), .. } => { RunEvent::WindowEvent { label: _label, event: WindowEvent::Focused(true), .. } => {
let h = app_handle.clone(); let h = app_handle.clone();
// Run update check whenever window is focused
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let val: State<'_, Mutex<YaakUpdater>> = h.state(); let val: State<'_, Mutex<YaakUpdater>> = h.state();
_ = val.lock().await.check(&h).await; _ = val.lock().await.check(&h).await;

View File

@@ -3,9 +3,9 @@ use std::fs;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use sqlx::types::{Json, JsonValue};
use sqlx::types::chrono::NaiveDateTime;
use tauri::AppHandle; use tauri::AppHandle;
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -54,6 +54,15 @@ pub struct HttpRequestHeader {
pub value: String, pub value: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpUrlParameter {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub name: String,
pub value: String,
}
fn default_http_request_method() -> String { fn default_http_request_method() -> String {
"GET".to_string() "GET".to_string()
} }
@@ -70,6 +79,7 @@ pub struct HttpRequest {
pub sort_priority: f64, pub sort_priority: f64,
pub name: String, pub name: String,
pub url: String, pub url: String,
pub url_parameters: Json<Vec<HttpUrlParameter>>,
#[serde(default = "default_http_request_method")] #[serde(default = "default_http_request_method")]
pub method: String, pub method: String,
pub body: Json<HashMap<String, JsonValue>>, pub body: Json<HashMap<String, JsonValue>>,
@@ -439,6 +449,7 @@ pub async fn upsert_request(
folder_id, folder_id,
name, name,
url, url,
url_parameters,
method, method,
body, body,
body_type, body_type,
@@ -447,7 +458,7 @@ pub async fn upsert_request(
headers, headers,
sort_priority sort_priority
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
name = excluded.name, name = excluded.name,
@@ -459,6 +470,7 @@ pub async fn upsert_request(
authentication = excluded.authentication, authentication = excluded.authentication,
authentication_type = excluded.authentication_type, authentication_type = excluded.authentication_type,
url = excluded.url, url = excluded.url,
url_parameters = excluded.url_parameters,
sort_priority = excluded.sort_priority sort_priority = excluded.sort_priority
"#, "#,
id, id,
@@ -466,6 +478,7 @@ pub async fn upsert_request(
r.folder_id, r.folder_id,
trimmed_name, trimmed_name,
r.url, r.url,
r.url_parameters,
r.method, r.method,
r.body, r.body,
r.body_type, r.body_type,
@@ -496,6 +509,7 @@ pub async fn find_requests(
updated_at, updated_at,
name, name,
url, url,
url_parameters AS "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
method, method,
body AS "body!: Json<HashMap<String, JsonValue>>", body AS "body!: Json<HashMap<String, JsonValue>>",
body_type, body_type,
@@ -525,6 +539,7 @@ pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, s
updated_at, updated_at,
name, name,
url, url,
url_parameters AS "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
method, method,
body AS "body!: Json<HashMap<String, JsonValue>>", body AS "body!: Json<HashMap<String, JsonValue>>",
body_type, body_type,

205
src-tauri/src/send.rs Normal file
View File

@@ -0,0 +1,205 @@
use std::fs::{create_dir_all, File};
use std::io::Write;
use base64::Engine;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use log::warn;
use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite};
use sqlx::types::Json;
use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err};
pub async fn actually_send_request(
request: models::HttpRequest,
response: &models::HttpResponse,
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
.expect("Failed to get Workspace");
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
for h in request.headers.0 {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header value: {}", e);
continue;
}
};
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" {
let raw_username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url_string.to_string()).headers(headers);
let mut query_params = Vec::new();
for p in request.url_parameters.0 {
if !p.enabled || p.name.is_empty() { continue; }
query_params.push((
render::render(&p.name, &workspace, environment_ref),
render::render(&p.value, &workspace, environment_ref),
));
}
request_builder = request_builder.query(&query_params);
if let Some(t) = &request.body_type {
let empty_value = &serde_json::to_value("").unwrap();
let b = request.body.0;
if b.contains_key("text") {
let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
request_builder = request_builder.body(body);
} else {
warn!("Unsupported body type: {}", t);
}
}
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
}
};
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = Json(
v.headers()
.iter()
.map(|(k, v)| models::HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap().to_string(),
})
.collect(),
);
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
{
// Write body to FS
let dir = app_handle.path_resolver().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
}
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
}
}

View File

@@ -4,7 +4,7 @@ use tauri::{AppHandle, updater, Window, Wry};
use tauri::api::dialog; use tauri::api::dialog;
// Check for updates every 3 hours // Check for updates every 3 hours
const MAX_UPDATE_CHECK_SECONDS: u64 = 3600 * 3; const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3;
// Create updater struct // Create updater struct
pub struct YaakUpdater { pub struct YaakUpdater {

View File

@@ -14,7 +14,7 @@ type Props = {
onChange: (headers: HttpRequest['headers']) => void; onChange: (headers: HttpRequest['headers']) => void;
}; };
export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) { export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) {
return ( return (
<PairEditor <PairEditor
valueAutocompleteVariables valueAutocompleteVariables

View File

@@ -3,17 +3,18 @@ import { PairEditor } from './core/PairEditor';
type Props = { type Props = {
forceUpdateKey: string; forceUpdateKey: string;
parameters: { name: string; value: string }[]; urlParameters: HttpRequest['headers'];
onChange: (headers: HttpRequest['headers']) => void; onChange: (headers: HttpRequest['urlParameters']) => void;
}; };
export function ParametersEditor({ parameters, forceUpdateKey, onChange }: Props) { export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
return ( return (
<PairEditor <PairEditor
forceUpdateKey={forceUpdateKey} valueAutocompleteVariables
pairs={parameters} nameAutocompleteVariables
pairs={urlParameters}
onChange={onChange} onChange={onChange}
namePlaceholder="name" forceUpdateKey={forceUpdateKey}
/> />
); );
} }

View File

@@ -10,7 +10,7 @@ import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models'; import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
import { import {
AUTH_TYPE_BASIC, AUTH_TYPE_BASIC,
AUTH_TYPE_BEARER, AUTH_TYPE_BEARER,
@@ -28,8 +28,8 @@ import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
import { GraphQLEditor } from './GraphQLEditor'; import { GraphQLEditor } from './GraphQLEditor';
import { HeaderEditor } from './HeaderEditor'; import { HeadersEditor } from './HeadersEditor';
import { ParametersEditor } from './ParameterEditor'; import { UrlParametersEditor } from './ParameterEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
interface Props { interface Props {
@@ -92,7 +92,15 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
}, },
}, },
}, },
// { value: 'params', label: 'URL Params' }, {
value: 'params',
label: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{ {
value: 'headers', value: 'headers',
label: ( label: (
@@ -141,6 +149,10 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
(headers: HttpHeader[]) => updateRequest.mutate({ headers }), (headers: HttpHeader[]) => updateRequest.mutate({ headers }),
[updateRequest], [updateRequest],
); );
const handleUrlParametersChange = useCallback(
(urlParameters: HttpUrlParameter[]) => updateRequest.mutate({ urlParameters }),
[updateRequest],
);
useListenToTauriEvent( useListenToTauriEvent(
'send_request', 'send_request',
@@ -189,17 +201,17 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
)} )}
</TabContent> </TabContent>
<TabContent value="headers"> <TabContent value="headers">
<HeaderEditor <HeadersEditor
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`} forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers} headers={activeRequest.headers}
onChange={handleHeadersChange} onChange={handleHeadersChange}
/> />
</TabContent> </TabContent>
<TabContent value="params"> <TabContent value="params">
<ParametersEditor <UrlParametersEditor
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
parameters={[]} urlParameters={activeRequest.urlParameters}
onChange={() => null} onChange={handleUrlParametersChange}
/> />
</TabContent> </TabContent>
<TabContent value="body"> <TabContent value="body">

View File

@@ -49,6 +49,12 @@ export interface HttpHeader {
enabled?: boolean; enabled?: boolean;
} }
export interface HttpUrlParameter {
name: string;
value: string;
enabled?: boolean;
}
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
readonly model: 'http_request'; readonly model: 'http_request';
@@ -56,6 +62,7 @@ export interface HttpRequest extends BaseModel {
sortPriority: number; sortPriority: number;
name: string; name: string;
url: string; url: string;
urlParameters: HttpUrlParameter[];
body: Record<string, string | number | boolean | null | undefined>; body: Record<string, string | number | boolean | null | undefined>;
bodyType: string | null; bodyType: string | null;
authentication: Record<string, string | number | boolean | null | undefined>; authentication: Record<string, string | number | boolean | null | undefined>;