mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-07 13:25:18 +02:00
Merge branch 'main' into codex/close-large-upload-responses
This commit is contained in:
@@ -191,12 +191,16 @@ fn build_url(r: &HttpRequest) -> String {
|
||||
fn append_graphql_query_params(url: &str, body: &BTreeMap<String, serde_json::Value>) -> String {
|
||||
let query = get_str_map(body, "query").to_string();
|
||||
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
||||
let operation_name = get_str_map(body, "operationName").to_string();
|
||||
let mut params = vec![("query".to_string(), query)];
|
||||
if !variables.trim().is_empty() {
|
||||
params.push(("variables".to_string(), variables));
|
||||
}
|
||||
if !operation_name.trim().is_empty() {
|
||||
params.push(("operationName".to_string(), operation_name));
|
||||
}
|
||||
// Strip existing query/variables params to avoid duplicates
|
||||
let url = strip_query_params(url, &["query", "variables"]);
|
||||
let url = strip_query_params(url, &["query", "variables", "operationName"]);
|
||||
append_query_params(&url, params)
|
||||
}
|
||||
|
||||
@@ -329,23 +333,30 @@ fn build_graphql_body(
|
||||
) -> Option<SendableBodyWithMeta> {
|
||||
let query = get_str_map(body, "query");
|
||||
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
||||
let operation_name = get_str_map(body, "operationName");
|
||||
|
||||
if method.to_lowercase() == "get" {
|
||||
// GraphQL GET requests use query parameters, not a body
|
||||
return None;
|
||||
}
|
||||
|
||||
let body = if variables.trim().is_empty() {
|
||||
format!(r#"{{"query":{}}}"#, serde_json::to_string(&query).unwrap_or_default())
|
||||
} else {
|
||||
format!(
|
||||
r#"{{"query":{},"variables":{}}}"#,
|
||||
serde_json::to_string(&query).unwrap_or_default(),
|
||||
variables
|
||||
)
|
||||
};
|
||||
let mut body = serde_json::Map::new();
|
||||
body.insert("query".to_string(), serde_json::Value::String(query.to_string()));
|
||||
if !variables.trim().is_empty() {
|
||||
body.insert(
|
||||
"variables".to_string(),
|
||||
serde_json::from_str(&variables)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(variables)),
|
||||
);
|
||||
}
|
||||
if !operation_name.trim().is_empty() {
|
||||
body.insert(
|
||||
"operationName".to_string(),
|
||||
serde_json::Value::String(operation_name.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(body)))
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(serde_json::to_string(&body).unwrap_or_default())))
|
||||
}
|
||||
|
||||
async fn build_multipart_body(
|
||||
@@ -522,6 +533,33 @@ mod tests {
|
||||
assert_eq!(result, "https://example.com/api?foo=bar&baz=qux");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_replaces_graphql_operation_name_from_body() {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("query".to_string(), json!("query Foo { foo } query Bar { bar }"));
|
||||
body.insert("operationName".to_string(), json!("Bar"));
|
||||
|
||||
let r = HttpRequest {
|
||||
method: "GET".to_string(),
|
||||
body_type: Some("graphql".to_string()),
|
||||
body,
|
||||
url: "https://example.com/graphql".to_string(),
|
||||
url_parameters: vec![HttpUrlParameter {
|
||||
enabled: true,
|
||||
name: "operationName".to_string(),
|
||||
value: "Foo".to_string(),
|
||||
id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = build_url(&r);
|
||||
assert_eq!(
|
||||
result,
|
||||
"https://example.com/graphql?query=query%20Foo%20%7B%20foo%20%7D%20query%20Bar%20%7B%20bar%20%7D&operationName=Bar",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_with_disabled_params() {
|
||||
let r = HttpRequest {
|
||||
@@ -880,9 +918,34 @@ mod tests {
|
||||
let result = build_graphql_body("POST", &body);
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
let expected =
|
||||
r#"{"query":"{ user(id: $id) { name } }","variables":{"id": "123"}}"#;
|
||||
assert_eq!(bytes, Bytes::from(expected));
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
|
||||
json!({
|
||||
"query": "{ user(id: $id) { name } }",
|
||||
"variables": { "id": "123" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_graphql_body_with_operation_name() {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("query".to_string(), json!("query Search { viewer { id } }"));
|
||||
body.insert("operationName".to_string(), json!("Search"));
|
||||
|
||||
let result = build_graphql_body("POST", &body);
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
|
||||
json!({
|
||||
"query": "query Search { viewer { id } }",
|
||||
"operationName": "Search",
|
||||
}),
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||
}
|
||||
|
||||
+1
@@ -402,6 +402,7 @@ export type Settings = {
|
||||
themeLight: string;
|
||||
updateChannel: string;
|
||||
hideLicenseBadge: boolean;
|
||||
promptFeedback: boolean;
|
||||
autoupdate: boolean;
|
||||
autoDownloadUpdates: boolean;
|
||||
checkNotifications: boolean;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add a setting to enable in-app feature feedback prompts
|
||||
ALTER TABLE settings
|
||||
ADD COLUMN prompt_feedback BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
@@ -5,7 +5,6 @@ use log::{debug, info};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::{OptionalExtension, params};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations");
|
||||
|
||||
@@ -25,23 +24,21 @@ impl BodyChunk {
|
||||
}
|
||||
|
||||
/// Manages the blob database connection pool.
|
||||
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
|
||||
// blocking `get()` serializes every blob access behind the slowest waiter, freezing the
|
||||
// whole app whenever the pool is exhausted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlobManager {
|
||||
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>,
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
impl BlobManager {
|
||||
pub fn new(pool: Pool<SqliteConnectionManager>) -> Self {
|
||||
Self { pool: Arc::new(Mutex::new(pool)) }
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> BlobContext {
|
||||
let conn = self
|
||||
.pool
|
||||
.lock()
|
||||
.expect("Failed to gain lock on blob DB")
|
||||
.get()
|
||||
.expect("Failed to get blob DB connection from pool");
|
||||
let conn = self.pool.get().expect("Failed to get blob DB connection from pool");
|
||||
BlobContext { conn }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,15 @@ pub fn init_standalone(
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Main database pool
|
||||
// Main database pool. Sized for concurrent in-flight queries, not concurrent app
|
||||
// features — connections are held per-statement, so even heavy fan-out (e.g. many
|
||||
// gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections
|
||||
// hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit.
|
||||
info!("Initializing app database {db_path:?}");
|
||||
let manager = sqlite_file_manager(db_path);
|
||||
let pool = Pool::builder()
|
||||
.max_size(100)
|
||||
.max_size(20)
|
||||
.min_idle(Some(2))
|
||||
.connection_timeout(Duration::from_secs(10))
|
||||
.build(manager)
|
||||
.map_err(|e| Error::Database(e.to_string()))?;
|
||||
@@ -70,7 +74,8 @@ pub fn init_standalone(
|
||||
// Blob database pool
|
||||
let blob_manager = sqlite_file_manager(blob_path);
|
||||
let blob_pool = Pool::builder()
|
||||
.max_size(50)
|
||||
.max_size(10)
|
||||
.min_idle(Some(1))
|
||||
.connection_timeout(Duration::from_secs(10))
|
||||
.build(blob_manager)
|
||||
.map_err(|e| Error::Database(e.to_string()))?;
|
||||
|
||||
@@ -246,6 +246,7 @@ pub struct Settings {
|
||||
pub theme_light: String,
|
||||
pub update_channel: String,
|
||||
pub hide_license_badge: bool,
|
||||
pub prompt_feedback: bool,
|
||||
pub autoupdate: bool,
|
||||
pub auto_download_updates: bool,
|
||||
pub check_notifications: bool,
|
||||
@@ -303,6 +304,7 @@ impl UpsertModelInfo for Settings {
|
||||
(ThemeLight, self.theme_light.as_str().into()),
|
||||
(UpdateChannel, self.update_channel.into()),
|
||||
(HideLicenseBadge, self.hide_license_badge.into()),
|
||||
(PromptFeedback, self.prompt_feedback.into()),
|
||||
(Autoupdate, self.autoupdate.into()),
|
||||
(AutoDownloadUpdates, self.auto_download_updates.into()),
|
||||
(ColoredMethods, self.colored_methods.into()),
|
||||
@@ -332,6 +334,7 @@ impl UpsertModelInfo for Settings {
|
||||
SettingsIden::ThemeLight,
|
||||
SettingsIden::UpdateChannel,
|
||||
SettingsIden::HideLicenseBadge,
|
||||
SettingsIden::PromptFeedback,
|
||||
SettingsIden::Autoupdate,
|
||||
SettingsIden::AutoDownloadUpdates,
|
||||
SettingsIden::ColoredMethods,
|
||||
@@ -372,6 +375,7 @@ impl UpsertModelInfo for Settings {
|
||||
autoupdate: row.get("autoupdate")?,
|
||||
auto_download_updates: row.get("auto_download_updates")?,
|
||||
hide_license_badge: row.get("hide_license_badge")?,
|
||||
prompt_feedback: row.get("prompt_feedback")?,
|
||||
colored_methods: row.get("colored_methods")?,
|
||||
check_notifications: row.get("check_notifications")?,
|
||||
hotkeys: serde_json::from_str(&hotkeys).unwrap_or_default(),
|
||||
|
||||
@@ -38,6 +38,7 @@ impl<'a> ClientDb<'a> {
|
||||
autoupdate: true,
|
||||
colored_methods: false,
|
||||
hide_license_badge: false,
|
||||
prompt_feedback: true,
|
||||
auto_download_updates: true,
|
||||
check_notifications: true,
|
||||
hotkeys: HashMap::new(),
|
||||
|
||||
@@ -4,27 +4,25 @@ use crate::util::ModelPayload;
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::TransactionBehavior;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::sync::mpsc;
|
||||
use yaak_database::{ConnectionOrTx, DbContext};
|
||||
|
||||
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
|
||||
// blocking `get()` serializes every DB access behind the slowest waiter, freezing the
|
||||
// whole app whenever the pool is exhausted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryManager {
|
||||
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>,
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
events_tx: mpsc::Sender<ModelPayload>,
|
||||
}
|
||||
|
||||
impl QueryManager {
|
||||
pub fn new(pool: Pool<SqliteConnectionManager>, events_tx: mpsc::Sender<ModelPayload>) -> Self {
|
||||
QueryManager { pool: Arc::new(Mutex::new(pool)), events_tx }
|
||||
QueryManager { pool, events_tx }
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> ClientDb<'_> {
|
||||
let conn = self
|
||||
.pool
|
||||
.lock()
|
||||
.expect("Failed to gain lock on DB")
|
||||
.get()
|
||||
.expect("Failed to get a new DB connection from the pool");
|
||||
let conn = self.pool.get().expect("Failed to get a new DB connection from the pool");
|
||||
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
|
||||
ClientDb::new(ctx, self.events_tx.clone())
|
||||
}
|
||||
@@ -33,12 +31,7 @@ impl QueryManager {
|
||||
where
|
||||
F: FnOnce(&ClientDb) -> T,
|
||||
{
|
||||
let conn = self
|
||||
.pool
|
||||
.lock()
|
||||
.expect("Failed to gain lock on DB for transaction")
|
||||
.get()
|
||||
.expect("Failed to get new DB connection from the pool");
|
||||
let conn = self.pool.get().expect("Failed to get new DB connection from the pool");
|
||||
|
||||
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
|
||||
let db = ClientDb::new(ctx, self.events_tx.clone());
|
||||
@@ -53,12 +46,7 @@ impl QueryManager {
|
||||
where
|
||||
E: From<crate::error::Error>,
|
||||
{
|
||||
let mut conn = self
|
||||
.pool
|
||||
.lock()
|
||||
.expect("Failed to gain lock on DB for transaction")
|
||||
.get()
|
||||
.expect("Failed to get new DB connection from the pool");
|
||||
let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool");
|
||||
let tx = conn
|
||||
.transaction_with_behavior(TransactionBehavior::Immediate)
|
||||
.expect("Failed to start DB transaction");
|
||||
|
||||
Reference in New Issue
Block a user