Route all database writes through one connection (#642)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-13 08:12:41 -07:00
committed by GitHub
co-authored by Claude Fable 5.1
parent ff7eebf3cd
commit 90cb578e26
62 changed files with 2231 additions and 1852 deletions
+13 -10
View File
@@ -49,8 +49,11 @@ fn schema(pretty: bool) -> CommandResult {
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult { fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?; let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?;
let environments = ctx let environments = ctx
.db() .query_manager()
.list_environments_ensure_base(&workspace_id) .with_tx(|tx| {
tx.ensure_base_environment(&workspace_id)?;
tx.list_environments(&workspace_id)
})
.map_err(|e| format!("Failed to list environments: {e}"))?; .map_err(|e| format!("Failed to list environments: {e}"))?;
if environments.is_empty() { if environments.is_empty() {
@@ -111,8 +114,8 @@ fn create(
} }
let created = ctx let created = ctx
.db() .query_manager()
.upsert_environment(&environment, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_environment(&environment, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create environment: {e}"))?; .map_err(|e| format!("Failed to create environment: {e}"))?;
println!("Created environment: {}", created.id); println!("Created environment: {}", created.id);
@@ -133,8 +136,8 @@ fn create(
}; };
let created = ctx let created = ctx
.db() .query_manager()
.upsert_environment(&environment, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_environment(&environment, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create environment: {e}"))?; .map_err(|e| format!("Failed to create environment: {e}"))?;
println!("Created environment: {}", created.id); println!("Created environment: {}", created.id);
@@ -152,8 +155,8 @@ fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) ->
let updated = apply_merge_patch(&existing, &patch, &id, "environment update")?; let updated = apply_merge_patch(&existing, &patch, &id, "environment update")?;
let saved = ctx let saved = ctx
.db() .query_manager()
.upsert_environment(&updated, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_environment(&updated, &UpdateSource::Sync))
.map_err(|e| format!("Failed to update environment: {e}"))?; .map_err(|e| format!("Failed to update environment: {e}"))?;
println!("Updated environment: {}", saved.id); println!("Updated environment: {}", saved.id);
@@ -167,8 +170,8 @@ fn delete(ctx: &CliContext, environment_id: &str, yes: bool) -> CommandResult {
} }
let deleted = ctx let deleted = ctx
.db() .query_manager()
.delete_environment_by_id(environment_id, &UpdateSource::Sync) .with_tx(|tx| tx.delete_environment_by_id(environment_id, &UpdateSource::Sync))
.map_err(|e| format!("Failed to delete environment: {e}"))?; .map_err(|e| format!("Failed to delete environment: {e}"))?;
println!("Deleted environment: {}", deleted.id); println!("Deleted environment: {}", deleted.id);
+8 -8
View File
@@ -102,8 +102,8 @@ fn create(
)?; )?;
let created = ctx let created = ctx
.db() .query_manager()
.upsert_folder(&folder, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create folder: {e}"))?; .map_err(|e| format!("Failed to create folder: {e}"))?;
println!("Created folder: {}", created.id); println!("Created folder: {}", created.id);
@@ -118,8 +118,8 @@ fn create(
let folder = Folder { workspace_id, name, ..Default::default() }; let folder = Folder { workspace_id, name, ..Default::default() };
let created = ctx let created = ctx
.db() .query_manager()
.upsert_folder(&folder, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create folder: {e}"))?; .map_err(|e| format!("Failed to create folder: {e}"))?;
println!("Created folder: {}", created.id); println!("Created folder: {}", created.id);
@@ -135,8 +135,8 @@ fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) ->
let updated = apply_merge_patch(&existing, &patch, &id, "folder update")?; let updated = apply_merge_patch(&existing, &patch, &id, "folder update")?;
let saved = ctx let saved = ctx
.db() .query_manager()
.upsert_folder(&updated, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_folder(&updated, &UpdateSource::Sync))
.map_err(|e| format!("Failed to update folder: {e}"))?; .map_err(|e| format!("Failed to update folder: {e}"))?;
println!("Updated folder: {}", saved.id); println!("Updated folder: {}", saved.id);
@@ -150,8 +150,8 @@ fn delete(ctx: &CliContext, folder_id: &str, yes: bool) -> CommandResult {
} }
let deleted = ctx let deleted = ctx
.db() .query_manager()
.delete_folder_by_id(folder_id, &UpdateSource::Sync) .with_tx(|tx| tx.delete_folder_by_id(folder_id, &UpdateSource::Sync))
.map_err(|e| format!("Failed to delete folder: {e}"))?; .map_err(|e| format!("Failed to delete folder: {e}"))?;
println!("Deleted folder: {}", deleted.id); println!("Deleted folder: {}", deleted.id);
+4 -2
View File
@@ -348,8 +348,9 @@ async fn install_from_directory(context: &CliContext, source: &str) -> CommandRe
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display())); ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
let plugin = context let plugin = context
.db() .query_manager()
.upsert_plugin( .with_tx(|tx| {
tx.upsert_plugin(
&Plugin { &Plugin {
directory: plugin_dir_str, directory: plugin_dir_str,
url: None, url: None,
@@ -359,6 +360,7 @@ async fn install_from_directory(context: &CliContext, source: &str) -> CommandRe
}, },
&UpdateSource::Background, &UpdateSource::Background,
) )
})
.map_err(|err| format!("Failed to save plugin in database: {err}"))?; .map_err(|err| format!("Failed to save plugin in database: {err}"))?;
let plugin_context = PluginContext::new(Some("cli".to_string()), None); let plugin_context = PluginContext::new(Some("cli".to_string()), None);
+8 -8
View File
@@ -424,8 +424,8 @@ fn create(
)?; )?;
let created = ctx let created = ctx
.db() .query_manager()
.upsert_http_request(&request, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create request: {e}"))?; .map_err(|e| format!("Failed to create request: {e}"))?;
println!("Created request: {}", created.id); println!("Created request: {}", created.id);
@@ -443,8 +443,8 @@ fn create(
} }
let created = ctx let created = ctx
.db() .query_manager()
.upsert_http_request(&request, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create request: {e}"))?; .map_err(|e| format!("Failed to create request: {e}"))?;
println!("Created request: {}", created.id); println!("Created request: {}", created.id);
@@ -463,8 +463,8 @@ fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) ->
let updated = apply_merge_patch(&existing, &patch, &id, "request update")?; let updated = apply_merge_patch(&existing, &patch, &id, "request update")?;
let saved = ctx let saved = ctx
.db() .query_manager()
.upsert_http_request(&updated, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_http_request(&updated, &UpdateSource::Sync))
.map_err(|e| format!("Failed to update request: {e}"))?; .map_err(|e| format!("Failed to update request: {e}"))?;
println!("Updated request: {}", saved.id); println!("Updated request: {}", saved.id);
@@ -487,8 +487,8 @@ fn delete(ctx: &CliContext, request_id: &str, yes: bool) -> CommandResult {
} }
let deleted = ctx let deleted = ctx
.db() .query_manager()
.delete_http_request_by_id(request_id, &UpdateSource::Sync) .with_tx(|tx| tx.delete_http_request_by_id(request_id, &UpdateSource::Sync))
.map_err(|e| format!("Failed to delete request: {e}"))?; .map_err(|e| format!("Failed to delete request: {e}"))?;
println!("Deleted request: {}", deleted.id); println!("Deleted request: {}", deleted.id);
Ok(()) Ok(())
+6 -4
View File
@@ -118,8 +118,8 @@ fn delete(ctx: &CliContext, id: &str, yes: bool) -> CommandResult {
return Ok(()); return Ok(());
} }
let count = ctx let count = ctx
.db() .query_manager()
.delete_all_http_responses_for_request(id, &UpdateSource::Sync) .with_tx(|tx| tx.delete_all_http_responses_for_request(id, &UpdateSource::Sync))
.map_err(|e| format!("Failed to delete responses: {e}"))?; .map_err(|e| format!("Failed to delete responses: {e}"))?;
println!("Deleted {count} responses for request {id}"); println!("Deleted {count} responses for request {id}");
return Ok(()); return Ok(());
@@ -131,8 +131,10 @@ fn delete(ctx: &CliContext, id: &str, yes: bool) -> CommandResult {
return Ok(()); return Ok(());
} }
let count = ctx let count = ctx
.db() .query_manager()
.delete_all_http_responses_for_workspace(&workspace_id, &UpdateSource::Sync) .with_tx(|tx| {
tx.delete_all_http_responses_for_workspace(&workspace_id, &UpdateSource::Sync)
})
.map_err(|e| format!("Failed to delete responses: {e}"))?; .map_err(|e| format!("Failed to delete responses: {e}"))?;
println!("Deleted {count} responses for workspace {workspace_id}"); println!("Deleted {count} responses for workspace {workspace_id}");
Ok(()) Ok(())
+10 -8
View File
@@ -84,8 +84,8 @@ fn create(
.map_err(|e| format!("Failed to parse workspace create JSON: {e}"))?; .map_err(|e| format!("Failed to parse workspace create JSON: {e}"))?;
let created = ctx let created = ctx
.db() .query_manager()
.upsert_workspace(&workspace, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create workspace: {e}"))?; .map_err(|e| format!("Failed to create workspace: {e}"))?;
println!("Created workspace: {}", created.id); println!("Created workspace: {}", created.id);
return Ok(()); return Ok(());
@@ -97,8 +97,8 @@ fn create(
let workspace = Workspace { name, ..Default::default() }; let workspace = Workspace { name, ..Default::default() };
let created = ctx let created = ctx
.db() .query_manager()
.upsert_workspace(&workspace, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
.map_err(|e| format!("Failed to create workspace: {e}"))?; .map_err(|e| format!("Failed to create workspace: {e}"))?;
println!("Created workspace: {}", created.id); println!("Created workspace: {}", created.id);
Ok(()) Ok(())
@@ -115,8 +115,8 @@ fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) ->
let updated = apply_merge_patch(&existing, &patch, &id, "workspace update")?; let updated = apply_merge_patch(&existing, &patch, &id, "workspace update")?;
let saved = ctx let saved = ctx
.db() .query_manager()
.upsert_workspace(&updated, &UpdateSource::Sync) .with_tx(|tx| tx.upsert_workspace(&updated, &UpdateSource::Sync))
.map_err(|e| format!("Failed to update workspace: {e}"))?; .map_err(|e| format!("Failed to update workspace: {e}"))?;
println!("Updated workspace: {}", saved.id); println!("Updated workspace: {}", saved.id);
@@ -130,8 +130,10 @@ fn delete(ctx: &CliContext, workspace_id: &str, yes: bool) -> CommandResult {
} }
let deleted = ctx let deleted = ctx
.db() .query_manager()
.delete_workspace_by_id(workspace_id, &UpdateSource::Sync, ctx.blob_manager()) .with_tx(|tx| {
tx.delete_workspace_by_id(workspace_id, &UpdateSource::Sync, ctx.blob_manager())
})
.map_err(|e| format!("Failed to delete workspace: {e}"))?; .map_err(|e| format!("Failed to delete workspace: {e}"))?;
println!("Deleted workspace: {}", deleted.id); println!("Deleted workspace: {}", deleted.id);
Ok(()) Ok(())
+3 -5
View File
@@ -51,11 +51,9 @@ impl CliContext {
}; };
// Guest: the desktop may have this DB open, so only what's safe beside a live session // Guest: the desktop may have this DB open, so only what's safe beside a live session
let _ = yaak_lifecycle::on_launch( let _ = query_manager.with_tx(|tx| {
&yaak_lifecycle::Host::guest(), yaak_lifecycle::on_launch(&yaak_lifecycle::Host::guest(), tx, &blob_manager)
&query_manager.connect(), });
&blob_manager,
);
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id)); let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
+5 -10
View File
@@ -40,8 +40,7 @@ pub fn seed_workspace(data_dir: &Path, workspace_id: &str) {
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
.upsert_workspace(&workspace, &UpdateSource::Sync)
.expect("Failed to seed workspace"); .expect("Failed to seed workspace");
} }
@@ -56,8 +55,7 @@ pub fn seed_request(data_dir: &Path, workspace_id: &str, request_id: &str) {
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
.upsert_http_request(&request, &UpdateSource::Sync)
.expect("Failed to seed request"); .expect("Failed to seed request");
} }
@@ -70,8 +68,7 @@ pub fn seed_folder(data_dir: &Path, workspace_id: &str, folder_id: &str) {
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
.upsert_folder(&folder, &UpdateSource::Sync)
.expect("Failed to seed folder"); .expect("Failed to seed folder");
} }
@@ -85,8 +82,7 @@ pub fn seed_grpc_request(data_dir: &Path, workspace_id: &str, request_id: &str)
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_grpc_request(&request, &UpdateSource::Sync))
.upsert_grpc_request(&request, &UpdateSource::Sync)
.expect("Failed to seed gRPC request"); .expect("Failed to seed gRPC request");
} }
@@ -100,7 +96,6 @@ pub fn seed_websocket_request(data_dir: &Path, workspace_id: &str, request_id: &
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_websocket_request(&request, &UpdateSource::Sync))
.upsert_websocket_request(&request, &UpdateSource::Sync)
.expect("Failed to seed WebSocket request"); .expect("Failed to seed WebSocket request");
} }
@@ -161,8 +161,7 @@ fn import_postman_environment_uses_workspace_id() {
let query_manager = query_manager(data_dir); let query_manager = query_manager(data_dir);
let db = query_manager.connect(); let db = query_manager.connect();
let environments = let environments = db.list_environments(&workspace_id).expect("list imported environments");
db.list_environments_ensure_base(&workspace_id).expect("list imported environments");
let imported_environment = let imported_environment =
environments.iter().find(|e| e.name == "Local").expect("postman environment imported"); environments.iter().find(|e| e.name == "Local").expect("postman environment imported");
@@ -299,7 +298,10 @@ fn re_import_leaves_deleted_resources_alone() {
.into_iter() .into_iter()
.find(|r| r.name == "Request B") .find(|r| r.name == "Request B")
.expect("request B imported"); .expect("request B imported");
db.delete_http_request_by_id(&request_b.id, &UpdateSource::Sync).expect("delete request B"); drop(db);
query_manager
.with_tx(|tx| tx.delete_http_request_by_id(&request_b.id, &UpdateSource::Sync))
.expect("delete request B");
workspace_id workspace_id
}; };
+1 -2
View File
@@ -25,8 +25,7 @@ fn top_level_send_folder_sends_http_requests_and_prints_summary() {
..Default::default() ..Default::default()
}; };
query_manager(data_dir) query_manager(data_dir)
.connect() .with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
.upsert_http_request(&request, &UpdateSource::Sync)
.expect("Failed to seed folder request"); .expect("Failed to seed folder request");
cli_cmd(data_dir) cli_cmd(data_dir)
+77 -46
View File
@@ -330,7 +330,8 @@ async fn cmd_grpc_go<R: Runtime>(
let settings = app_handle.db().get_settings(); let settings = app_handle.db().get_settings();
let client_cert = find_client_certificate(&request.url, &settings.client_certificates); let client_cert = find_client_certificate(&request.url, &settings.client_certificates);
let conn = app_handle.db().upsert_grpc_connection( let conn = app_handle.with_tx(|tx| {
tx.upsert_grpc_connection(
&GrpcConnection { &GrpcConnection {
workspace_id: request.workspace_id.clone(), workspace_id: request.workspace_id.clone(),
request_id: request.id.clone(), request_id: request.id.clone(),
@@ -341,7 +342,8 @@ async fn cmd_grpc_go<R: Runtime>(
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
let conn_id = conn.id.clone(); let conn_id = conn.id.clone();
@@ -386,7 +388,8 @@ async fn cmd_grpc_go<R: Runtime>(
let connection = match connection { let connection = match connection {
Ok(c) => c, Ok(c) => c,
Err(err) => { Err(err) => {
app_handle.db().upsert_grpc_connection( app_handle.with_tx(|tx| {
tx.upsert_grpc_connection(
&GrpcConnection { &GrpcConnection {
elapsed: start.elapsed().as_millis() as i32, elapsed: start.elapsed().as_millis() as i32,
error: Some(err.to_string()), error: Some(err.to_string()),
@@ -394,7 +397,8 @@ async fn cmd_grpc_go<R: Runtime>(
..conn.clone() ..conn.clone()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
return Ok(conn_id); return Ok(conn_id);
} }
}; };
@@ -495,7 +499,8 @@ async fn cmd_grpc_go<R: Runtime>(
.await?; .await?;
let msg = strip_json_comments(&msg); let msg = strip_json_comments(&msg);
app_handle.db().upsert_grpc_event( app_handle.with_tx(|tx| {
tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: format!("Connecting to {}", req.url), content: format!("Connecting to {}", req.url),
event_type: GrpcEventType::ConnectionStart, event_type: GrpcEventType::ConnectionStart,
@@ -503,7 +508,8 @@ async fn cmd_grpc_go<R: Runtime>(
..base_event.clone() ..base_event.clone()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
async move { async move {
// Create callback for streaming methods that handles both success and error // Create callback for streaming methods that handles both success and error
@@ -513,24 +519,28 @@ async fn cmd_grpc_go<R: Runtime>(
let window_label = window.label().to_string(); let window_label = window.label().to_string();
move |result: std::result::Result<String, String>| match result { move |result: std::result::Result<String, String>| match result {
Ok(msg) => { Ok(msg) => {
let _ = app_handle.db().upsert_grpc_event( let _ = app_handle.with_tx(|tx| {
tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: msg, content: msg,
event_type: GrpcEventType::ClientMessage, event_type: GrpcEventType::ClientMessage,
..base_event.clone() ..base_event.clone()
}, },
&UpdateSource::from_window_label(&window_label), &UpdateSource::from_window_label(&window_label),
); )
});
} }
Err(error) => { Err(error) => {
let _ = app_handle.db().upsert_grpc_event( let _ = app_handle.with_tx(|tx| {
tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: format!("Failed to send message: {}", error), content: format!("Failed to send message: {}", error),
event_type: GrpcEventType::Error, event_type: GrpcEventType::Error,
..base_event.clone() ..base_event.clone()
}, },
&UpdateSource::from_window_label(&window_label), &UpdateSource::from_window_label(&window_label),
); )
});
} }
} }
}; };
@@ -583,8 +593,8 @@ async fn cmd_grpc_go<R: Runtime>(
if !method_desc.is_client_streaming() { if !method_desc.is_client_streaming() {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
event_type: GrpcEventType::ClientMessage, event_type: GrpcEventType::ClientMessage,
content: msg, content: msg,
@@ -592,14 +602,15 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
} }
match maybe_msg { match maybe_msg {
Some(Ok(msg)) => { Some(Ok(msg)) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
metadata: metadata_to_map(msg.metadata().clone()), metadata: metadata_to_map(msg.metadata().clone()),
content: if msg.metadata().len() == 0 { content: if msg.metadata().len() == 0 {
@@ -613,6 +624,7 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
let response_message = msg.into_inner(); let response_message = msg.into_inner();
let content = match connection let content = match connection
@@ -622,8 +634,8 @@ async fn cmd_grpc_go<R: Runtime>(
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Failed to read response".to_string(), content: "Failed to read response".to_string(),
error: Some(err.to_string()), error: Some(err.to_string()),
@@ -633,13 +645,14 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
return; return;
} }
}; };
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content, content,
event_type: GrpcEventType::ServerMessage, event_type: GrpcEventType::ServerMessage,
@@ -647,10 +660,11 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Connection complete".to_string(), content: "Connection complete".to_string(),
event_type: GrpcEventType::ConnectionEnd, event_type: GrpcEventType::ConnectionEnd,
@@ -659,12 +673,13 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
} }
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => { Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&(match e.status { &(match e.status {
Some(s) => GrpcEvent { Some(s) => GrpcEvent {
error: Some(s.message().to_string()), error: Some(s.message().to_string()),
@@ -684,12 +699,13 @@ async fn cmd_grpc_go<R: Runtime>(
}), }),
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
} }
Some(Err(e)) => { Some(Err(e)) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
error: Some(e.to_string()), error: Some(e.to_string()),
status: Some(Code::Unknown as i32), status: Some(Code::Unknown as i32),
@@ -699,6 +715,7 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
} }
None => { None => {
@@ -709,8 +726,8 @@ async fn cmd_grpc_go<R: Runtime>(
let mut stream = match maybe_stream { let mut stream = match maybe_stream {
Some(Ok(stream)) => { Some(Ok(stream)) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
metadata: metadata_to_map(stream.metadata().clone()), metadata: metadata_to_map(stream.metadata().clone()),
content: if stream.metadata().len() == 0 { content: if stream.metadata().len() == 0 {
@@ -724,14 +741,15 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
stream.into_inner() stream.into_inner()
} }
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => { Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
warn!("GRPC stream error {e:?}"); warn!("GRPC stream error {e:?}");
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&(match e.status { &(match e.status {
Some(s) => GrpcEvent { Some(s) => GrpcEvent {
error: Some(s.message().to_string()), error: Some(s.message().to_string()),
@@ -751,13 +769,14 @@ async fn cmd_grpc_go<R: Runtime>(
}), }),
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
return; return;
} }
Some(Err(e)) => { Some(Err(e)) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
error: Some(e.to_string()), error: Some(e.to_string()),
status: Some(Code::Unknown as i32), status: Some(Code::Unknown as i32),
@@ -767,6 +786,7 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
return; return;
} }
@@ -783,8 +803,8 @@ async fn cmd_grpc_go<R: Runtime>(
Ok(message) => message, Ok(message) => message,
Err(err) => { Err(err) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Failed to read response".to_string(), content: "Failed to read response".to_string(),
error: Some(err.to_string()), error: Some(err.to_string()),
@@ -794,13 +814,14 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
break; break;
} }
}; };
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: message, content: message,
event_type: GrpcEventType::ServerMessage, event_type: GrpcEventType::ServerMessage,
@@ -808,14 +829,15 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
} }
Ok(None) => { Ok(None) => {
let trailers = let trailers =
stream.trailers().await.unwrap_or_default().unwrap_or_default(); stream.trailers().await.unwrap_or_default().unwrap_or_default();
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Connection complete".to_string(), content: "Connection complete".to_string(),
status: Some(Code::Ok as i32), status: Some(Code::Ok as i32),
@@ -825,13 +847,14 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
break; break;
} }
Err(status) => { Err(status) => {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_grpc_event( tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Stream failed".to_string(), content: "Stream failed".to_string(),
error: Some(status.message().to_string()), error: Some(status.message().to_string()),
@@ -842,6 +865,7 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
) )
})
.unwrap(); .unwrap();
break; break;
} }
@@ -874,7 +898,8 @@ async fn cmd_grpc_go<R: Runtime>(
}).unwrap(); }).unwrap();
}, },
_ = cancelled_rx.changed() => { _ = cancelled_rx.changed() => {
w.db().upsert_grpc_event( w.with_tx(|tx| {
tx.upsert_grpc_event(
&GrpcEvent { &GrpcEvent {
content: "Cancelled".to_string(), content: "Cancelled".to_string(),
event_type: GrpcEventType::ConnectionEnd, event_type: GrpcEventType::ConnectionEnd,
@@ -882,7 +907,9 @@ async fn cmd_grpc_go<R: Runtime>(
..base_msg.clone() ..base_msg.clone()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
).unwrap(); )
})
.unwrap();
w.with_tx(|c| { w.with_tx(|c| {
c.upsert_grpc_connection( c.upsert_grpc_connection(
&GrpcConnection{ &GrpcConnection{
@@ -1066,7 +1093,8 @@ async fn cmd_send_http_request<R: Runtime>(
let request = app_handle.db().get_http_request(&request_id)?; let request = app_handle.db().get_http_request(&request_id)?;
let blobs = app_handle.blob_manager(); let blobs = app_handle.blob_manager();
let response = app_handle.db().upsert_http_response( let response = app_handle.with_tx(|tx| {
tx.upsert_http_response(
&HttpResponse { &HttpResponse {
request_id: request.id.clone(), request_id: request.id.clone(),
workspace_id: request.workspace_id.clone(), workspace_id: request.workspace_id.clone(),
@@ -1074,7 +1102,8 @@ async fn cmd_send_http_request<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
&blobs, &blobs,
)?; )
})?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
app_handle.listen_any(format!("cancel_http_response_{}", response.id), move |_event| { app_handle.listen_any(format!("cancel_http_response_{}", response.id), move |_event| {
@@ -1112,7 +1141,8 @@ async fn cmd_send_http_request<R: Runtime>(
Ok(sent) => sent.response, Ok(sent) => sent.response,
Err(e) => { Err(e) => {
let resp = app_handle.db().get_http_response(&response.id)?; let resp = app_handle.db().get_http_response(&response.id)?;
app_handle.db().upsert_http_response( app_handle.with_tx(|tx| {
tx.upsert_http_response(
&HttpResponse { &HttpResponse {
state: HttpResponseState::Closed, state: HttpResponseState::Closed,
error: Some(e.to_string()), error: Some(e.to_string()),
@@ -1120,7 +1150,8 @@ async fn cmd_send_http_request<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
&blobs, &blobs,
)? )
})?
} }
}; };
@@ -1254,8 +1285,8 @@ pub fn run() {
.setup(|app| { .setup(|app| {
let lifecycle_host = yaak_lifecycle::Host::owner() let lifecycle_host = yaak_lifecycle::Host::owner()
.with_responses_dir(app.path().app_data_dir()?.join("responses")); .with_responses_dir(app.path().app_data_dir()?.join("responses"));
if let Err(e) = if let Err(e) = app
yaak_lifecycle::on_launch(&lifecycle_host, &app.db(), &app.blob_manager()) .with_tx(|tx| yaak_lifecycle::on_launch(&lifecycle_host, tx, &app.blob_manager()))
{ {
error!("on_launch hook failed: {e:?}"); error!("on_launch hook failed: {e:?}");
} }
@@ -10,7 +10,7 @@ use tauri::plugin::TauriPlugin;
use tauri::{Emitter, Manager, Runtime, State}; use tauri::{Emitter, Manager, Runtime, State};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use yaak_models::blob_manager::BlobManager; use yaak_models::blob_manager::BlobManager;
use yaak_models::client_db::ClientDb; use yaak_models::client_db::{ClientDb, WriteDb};
use yaak_models::error::Result; use yaak_models::error::Result;
use yaak_models::query_manager::QueryManager; use yaak_models::query_manager::QueryManager;
use yaak_models::util::{ModelPayload, UpdateSource}; use yaak_models::util::{ModelPayload, UpdateSource};
@@ -95,7 +95,7 @@ pub trait QueryManagerExt<'a, R> {
fn db(&'a self) -> ClientDb<'a>; fn db(&'a self) -> ClientDb<'a>;
fn with_tx<F, T>(&'a self, func: F) -> Result<T> fn with_tx<F, T>(&'a self, func: F) -> Result<T>
where where
F: FnOnce(&ClientDb) -> Result<T>; F: FnOnce(&WriteDb) -> Result<T>;
} }
impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M { impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
@@ -110,7 +110,7 @@ impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
fn with_tx<F, T>(&'a self, func: F) -> Result<T> fn with_tx<F, T>(&'a self, func: F) -> Result<T>
where where
F: FnOnce(&ClientDb) -> Result<T>, F: FnOnce(&WriteDb) -> Result<T>,
{ {
let qm = self.state::<QueryManager>(); let qm = self.state::<QueryManager>();
qm.inner().with_tx(func) qm.inner().with_tx(func)
@@ -55,13 +55,16 @@ impl YaakNotifier {
seen.push(id.to_string()); seen.push(id.to_string());
debug!("Marked notification as seen {}", id); debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen)?; let seen_json = serde_json::to_string(&seen)?;
window.db().set_key_value_raw( window.with_tx(|tx| {
tx.set_key_value_raw(
KV_NAMESPACE, KV_NAMESPACE,
KV_KEY, KV_KEY,
seen_json.as_str(), seen_json.as_str(),
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
); );
Ok(()) Ok(())
})?;
Ok(())
} }
pub async fn maybe_check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> { pub async fn maybe_check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> {
@@ -111,7 +111,7 @@ async fn handle_host_plugin_request<R: Runtime>(
} }
let new_plugin = Plugin { updated_at: Utc::now().naive_utc(), ..plugin }; let new_plugin = Plugin { updated_at: Utc::now().naive_utc(), ..plugin };
app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin)?; app_handle.with_tx(|tx| tx.upsert_plugin(&new_plugin, &UpdateSource::Plugin))?;
} }
if !req.silent { if !req.silent {
@@ -294,7 +294,8 @@ async fn handle_host_plugin_request<R: Runtime>(
HttpResponse::default() HttpResponse::default()
} else { } else {
let blobs = window.blob_manager(); let blobs = window.blob_manager();
window.db().upsert_http_response( window.with_tx(|tx| {
tx.upsert_http_response(
&HttpResponse { &HttpResponse {
request_id: http_request.id.clone(), request_id: http_request.id.clone(),
workspace_id: http_request.workspace_id.clone(), workspace_id: http_request.workspace_id.clone(),
@@ -302,7 +303,8 @@ async fn handle_host_plugin_request<R: Runtime>(
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
&blobs, &blobs,
)? )
})?
}; };
let http_response = send_http_request_with_context( let http_response = send_http_request_with_context(
@@ -202,7 +202,8 @@ pub async fn cmd_plugins_install_from_directory<R: Runtime>(
// Resolve the manager before writing the row so startup's plugin snapshot // Resolve the manager before writing the row so startup's plugin snapshot
// can't include it and boot it a second time // can't include it and boot it a second time
let plugin_manager = Arc::new(plugin_manager(&window).await?); let plugin_manager = Arc::new(plugin_manager(&window).await?);
let plugin = window.db().upsert_plugin( let plugin = window.with_tx(|tx| {
tx.upsert_plugin(
&Plugin { &Plugin {
directory: directory.into(), directory: directory.into(),
url: None, url: None,
@@ -211,7 +212,8 @@ pub async fn cmd_plugins_install_from_directory<R: Runtime>(
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?; plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?;
+8 -5
View File
@@ -12,8 +12,8 @@ use tokio::sync::watch;
use yaak_rpc_schema::WatchResult; use yaak_rpc_schema::WatchResult;
use yaak_sync::error::Error::InvalidSyncDirectory; use yaak_sync::error::Error::InvalidSyncDirectory;
use yaak_sync::sync::{ use yaak_sync::sync::{
FsCandidate, SyncOp, apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates, FsCandidate, SyncOp, apply_db_sync_ops, apply_fs_sync_ops, apply_sync_state_ops,
get_fs_candidates, compute_sync_ops, get_db_candidates, get_fs_candidates,
}; };
use yaak_sync::watch::{WatchEvent, watch_directory}; use yaak_sync::watch::{WatchEvent, watch_directory};
@@ -49,11 +49,14 @@ pub(crate) async fn cmd_sync_apply<R: Runtime>(
sync_dir: &Path, sync_dir: &Path,
workspace_id: &str, workspace_id: &str,
) -> Result<()> { ) -> Result<()> {
let db = app_handle.db(); // Files first, so the write transaction never waits on the filesystem
let pending = apply_fs_sync_ops(workspace_id, sync_dir, sync_ops)?;
let blobs = app_handle.blob_manager(); let blobs = app_handle.blob_manager();
let sync_state_ops = apply_sync_ops(&db, &blobs, workspace_id, sync_dir, sync_ops)?; app_handle.db_manager().with_tx(|tx| {
apply_sync_state_ops(&db, workspace_id, sync_dir, sync_state_ops)?; let sync_state_ops = apply_db_sync_ops(tx, &blobs, workspace_id, sync_dir, pending)?;
apply_sync_state_ops(tx, workspace_id, sync_dir, sync_state_ops)?;
Ok(()) Ok(())
})
} }
pub(crate) async fn sync_watch<R, F>( pub(crate) async fn sync_watch<R, F>(
+43 -27
View File
@@ -43,7 +43,8 @@ pub async fn cmd_ws_send<R: Runtime>(
{ {
Ok(connection) => Ok(connection), Ok(connection) => Ok(connection),
Err(e) => { Err(e) => {
app_handle.db().upsert_websocket_event( app_handle.with_tx(|tx| {
tx.upsert_websocket_event(
&WebsocketEvent { &WebsocketEvent {
connection_id: connection.id.clone(), connection_id: connection.id.clone(),
request_id: connection.request_id.clone(), request_id: connection.request_id.clone(),
@@ -54,7 +55,8 @@ pub async fn cmd_ws_send<R: Runtime>(
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
Ok(connection) Ok(connection)
} }
@@ -96,7 +98,8 @@ async fn send_websocket_message<R: Runtime>(
let mut ws_manager = ws_manager.lock().await; let mut ws_manager = ws_manager.lock().await;
ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?; ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?;
app_handle.db().upsert_websocket_event( app_handle.with_tx(|tx| {
tx.upsert_websocket_event(
&WebsocketEvent { &WebsocketEvent {
connection_id: connection.id.clone(), connection_id: connection.id.clone(),
request_id: request.id.clone(), request_id: request.id.clone(),
@@ -107,7 +110,8 @@ async fn send_websocket_message<R: Runtime>(
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
Ok(connection.clone()) Ok(connection.clone())
} }
@@ -118,14 +122,13 @@ pub async fn cmd_ws_close<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
ws_manager: State<'_, Mutex<WebsocketManager>>, ws_manager: State<'_, Mutex<WebsocketManager>>,
) -> Result<WebsocketConnection> { ) -> Result<WebsocketConnection> {
let connection = { let connection = app_handle.with_tx(|tx| {
let db = app_handle.db(); let connection = tx.get_websocket_connection(connection_id)?;
let connection = db.get_websocket_connection(connection_id)?; tx.upsert_websocket_connection(
db.upsert_websocket_connection(
&WebsocketConnection { state: WebsocketConnectionState::Closing, ..connection }, &WebsocketConnection { state: WebsocketConnectionState::Closing, ..connection },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)? )
}; })?;
let mut ws_manager = ws_manager.lock().await; let mut ws_manager = ws_manager.lock().await;
if let Err(e) = ws_manager.close(&connection.id).await { if let Err(e) = ws_manager.close(&connection.id).await {
@@ -169,14 +172,16 @@ pub async fn cmd_ws_connect<R: Runtime>(
) )
.await?; .await?;
let connection = app_handle.db().upsert_websocket_connection( let connection = app_handle.with_tx(|tx| {
tx.upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
workspace_id: request.workspace_id.clone(), workspace_id: request.workspace_id.clone(),
request_id: request_id.to_string(), request_id: request_id.to_string(),
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
let (mut url, url_parameters) = apply_path_placeholders(&request.url, &request.url_parameters); let (mut url, url_parameters) = apply_path_placeholders(&request.url, &request.url_parameters);
if !url.starts_with("ws://") && !url.starts_with("wss://") { if !url.starts_with("ws://") && !url.starts_with("wss://") {
@@ -187,14 +192,16 @@ pub async fn cmd_ws_connect<R: Runtime>(
let mut url = match Url::parse(&url) { let mut url = match Url::parse(&url) {
Ok(url) => url, Ok(url) => url,
Err(e) => { Err(e) => {
return Ok(app_handle.db().upsert_websocket_connection( return Ok(app_handle.with_tx(|tx| {
tx.upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
error: Some(format!("Failed to parse URL {}", e.to_string())), error: Some(format!("Failed to parse URL {}", e.to_string())),
state: WebsocketConnectionState::Closed, state: WebsocketConnectionState::Closed,
..connection ..connection
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?); )
})?);
} }
}; };
@@ -321,18 +328,21 @@ pub async fn cmd_ws_connect<R: Runtime>(
{ {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
return Ok(app_handle.db().upsert_websocket_connection( return Ok(app_handle.with_tx(|tx| {
tx.upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
error: Some(e.to_string()), error: Some(e.to_string()),
state: WebsocketConnectionState::Closed, state: WebsocketConnectionState::Closed,
..connection ..connection
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?); )
})?);
} }
}; };
app_handle.db().upsert_websocket_event( app_handle.with_tx(|tx| {
tx.upsert_websocket_event(
&WebsocketEvent { &WebsocketEvent {
connection_id: connection.id.clone(), connection_id: connection.id.clone(),
request_id: request.id.clone(), request_id: request.id.clone(),
@@ -342,7 +352,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
..Default::default() ..Default::default()
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
let response_headers = response let response_headers = response
.headers() .headers()
@@ -366,11 +377,12 @@ pub async fn cmd_ws_connect<R: Runtime>(
if !set_cookie_headers.is_empty() { if !set_cookie_headers.is_empty() {
store.store_cookies_from_response(&convert_ws_url_to_http(&url), &set_cookie_headers); store.store_cookies_from_response(&convert_ws_url_to_http(&url), &set_cookie_headers);
cookie_jar.cookies = store.get_all_cookies(); cookie_jar.cookies = store.get_all_cookies();
app_handle.db().upsert_cookie_jar(cookie_jar, &UpdateSource::Background)?; app_handle.with_tx(|tx| tx.upsert_cookie_jar(cookie_jar, &UpdateSource::Background))?;
} }
} }
let connection = app_handle.db().upsert_websocket_connection( let connection = app_handle.with_tx(|tx| {
tx.upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
state: WebsocketConnectionState::Connected, state: WebsocketConnectionState::Connected,
headers: response_headers, headers: response_headers,
@@ -379,7 +391,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
..connection ..connection
}, },
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
{ {
let connection_id = connection.id.clone(); let connection_id = connection.id.clone();
@@ -395,8 +408,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
} }
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_websocket_event( tx.upsert_websocket_event(
&WebsocketEvent { &WebsocketEvent {
connection_id: connection_id.clone(), connection_id: connection_id.clone(),
request_id: request_id.clone(), request_id: request_id.clone(),
@@ -416,13 +429,14 @@ pub async fn cmd_ws_connect<R: Runtime>(
}, },
&UpdateSource::from_window_label(&window_label), &UpdateSource::from_window_label(&window_label),
) )
})
.unwrap(); .unwrap();
} }
info!("Websocket connection closed"); info!("Websocket connection closed");
if !has_written_close { if !has_written_close {
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_websocket_event( tx.upsert_websocket_event(
&WebsocketEvent { &WebsocketEvent {
connection_id: connection_id.clone(), connection_id: connection_id.clone(),
request_id: request_id.clone(), request_id: request_id.clone(),
@@ -433,11 +447,12 @@ pub async fn cmd_ws_connect<R: Runtime>(
}, },
&UpdateSource::from_window_label(&window_label), &UpdateSource::from_window_label(&window_label),
) )
})
.unwrap(); .unwrap();
} }
app_handle app_handle
.db() .with_tx(|tx| {
.upsert_websocket_connection( tx.upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
workspace_id: request.workspace_id.clone(), workspace_id: request.workspace_id.clone(),
request_id: request_id.to_string(), request_id: request_id.to_string(),
@@ -446,6 +461,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
}, },
&UpdateSource::from_window_label(&window_label), &UpdateSource::from_window_label(&window_label),
) )
})
.unwrap(); .unwrap();
}); });
} }
+23 -4
View File
@@ -9,7 +9,7 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
use ts_rs::TS; use ts_rs::TS;
use yaak_api::{ApiClientKind, yaak_api_client}; use yaak_api::{ApiClientKind, yaak_api_client};
use yaak_common::platform::get_os_str; use yaak_common::platform::get_os_str;
use yaak_models::client_db::ClientDb; use yaak_models::client_db::{ClientDb, WriteDb};
use yaak_models::query_manager::QueryManager; use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -17,6 +17,10 @@ use yaak_models::util::UpdateSource;
/// This is needed temporarily until all crates are refactored to not use Tauri. /// This is needed temporarily until all crates are refactored to not use Tauri.
trait QueryManagerExt<'a, R> { trait QueryManagerExt<'a, R> {
fn db(&'a self) -> ClientDb<'a>; fn db(&'a self) -> ClientDb<'a>;
fn with_tx<T>(
&'a self,
func: impl FnOnce(&WriteDb) -> yaak_models::error::Result<T>,
) -> yaak_models::error::Result<T>;
} }
impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M { impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
@@ -24,6 +28,14 @@ impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
let qm = self.state::<QueryManager>(); let qm = self.state::<QueryManager>();
qm.inner().connect() qm.inner().connect()
} }
fn with_tx<T>(
&'a self,
func: impl FnOnce(&WriteDb) -> yaak_models::error::Result<T>,
) -> yaak_models::error::Result<T> {
let qm = self.state::<QueryManager>();
qm.inner().with_tx(func)
}
} }
const KV_NAMESPACE: &str = "license"; const KV_NAMESPACE: &str = "license";
@@ -137,12 +149,17 @@ pub async fn activate_license<R: Runtime>(
} }
let body: ActivateLicenseResponsePayload = response.json().await?; let body: ActivateLicenseResponsePayload = response.json().await?;
window.app_handle().db().set_key_value_str( if let Err(e) = window.app_handle().with_tx(|tx| {
tx.set_key_value_str(
KV_ACTIVATION_ID_KEY, KV_ACTIVATION_ID_KEY,
KV_NAMESPACE, KV_NAMESPACE,
body.activation_id.as_str(), body.activation_id.as_str(),
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
); );
Ok(())
}) {
warn!("Failed to store license activation: {e}");
}
if let Err(e) = window.emit("license-activated", true) { if let Err(e) = window.emit("license-activated", true) {
warn!("Failed to emit check-license event: {}", e); warn!("Failed to emit check-license event: {}", e);
@@ -172,11 +189,13 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
return Err(ServerError); return Err(ServerError);
} }
app_handle.db().delete_key_value( app_handle.with_tx(|tx| {
tx.delete_key_value(
KV_ACTIVATION_ID_KEY, KV_ACTIVATION_ID_KEY,
KV_NAMESPACE, KV_NAMESPACE,
&UpdateSource::from_window_label(window.label()), &UpdateSource::from_window_label(window.label()),
)?; )
})?;
if let Err(e) = app_handle.emit("license-deactivated", true) { if let Err(e) = app_handle.emit("license-deactivated", true) {
warn!("Failed to emit deactivate-license event: {}", e); warn!("Failed to emit deactivate-license event: {}", e);
+2 -1
View File
@@ -21,7 +21,8 @@
//! the second ask. Sharing the handle instead makes nested *reads* work the way //! the second ask. Sharing the handle instead makes nested *reads* work the way
//! they do on the desktop; nested *write transactions* fail on both, only //! they do on the desktop; nested *write transactions* fail on both, only
//! differently (here SQLite refuses the inner `BEGIN`; natively the inner //! differently (here SQLite refuses the inner `BEGIN`; natively the inner
//! connection blocks on `busy_timeout` and then fails). //! call waits for the one writer connection, which the outer call holds, and
//! times out).
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod imp { mod imp {
+1 -1
View File
@@ -72,7 +72,7 @@ pub trait Host: Clone {
self.query_manager().connect() self.query_manager().connect()
} }
fn blobs(&self) -> BlobContext { fn blobs(&self) -> BlobContext<'_> {
self.blob_manager().connect() self.blob_manager().connect()
} }
} }
+25 -17
View File
@@ -11,10 +11,11 @@ use yaak_models::queries::workspaces::default_headers;
use yaak_rpc_schema::*; use yaak_rpc_schema::*;
pub async fn models_upsert<H: Host>(host: H, req: ModelsUpsertReq) -> Result<String> { pub async fn models_upsert<H: Host>(host: H, req: ModelsUpsertReq) -> Result<String> {
let db = host.db();
let blobs = host.blob_manager(); let blobs = host.blob_manager();
let source = host.update_source(); let source = host.update_source();
Ok(yaak_models::models_ops::upsert_model(&db, blobs, req.model, &source)?) Ok(host
.query_manager()
.with_tx(|tx| yaak_models::models_ops::upsert_model(tx, blobs, req.model, &source))?)
} }
/// Deletes cascade — a workspace can hold thousands of requests — and run in a /// Deletes cascade — a workspace can hold thousands of requests — and run in a
@@ -75,12 +76,9 @@ pub async fn models_upsert_graphql_introspection<H: Host>(
req: ModelsUpsertGraphqlIntrospectionReq, req: ModelsUpsertGraphqlIntrospectionReq,
) -> Result<GraphQlIntrospection> { ) -> Result<GraphQlIntrospection> {
let source = host.update_source(); let source = host.update_source();
Ok(host.db().upsert_graphql_introspection( Ok(host.query_manager().with_tx(|tx| {
&req.workspace_id, tx.upsert_graphql_introspection(&req.workspace_id, &req.request_id, req.content, &source)
&req.request_id, })?)
req.content,
&source,
)?)
} }
/// Everything the frontend's model store needs to boot, as one JSON string. /// Everything the frontend's model store needs to boot, as one JSON string.
@@ -112,9 +110,16 @@ pub async fn models_workspace_models<H: PluginHost>(
// Add the workspace children // Add the workspace children
if let Some(wid) = req.workspace_id.as_deref() { if let Some(wid) = req.workspace_id.as_deref() {
// Opening a workspace is where the rows it is assumed to have get created
host.query_manager().with_tx(|tx| {
tx.ensure_base_environment(wid)?;
tx.ensure_default_cookie_jar(wid)?;
tx.ensure_workspace_meta(wid)?;
Ok::<(), yaak_models::error::Error>(())
})?;
let db = host.db(); let db = host.db();
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect()); l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect()); l.append(&mut db.list_environments(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect()); l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_connections(wid)?.into_iter().map(Into::into).collect()); l.append(&mut db.list_grpc_connections(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_requests(wid)?.into_iter().map(Into::into).collect()); l.append(&mut db.list_grpc_requests(wid)?.into_iter().map(Into::into).collect());
@@ -132,23 +137,26 @@ pub async fn cmd_get_workspace_meta<H: Host>(
host: H, host: H,
req: CmdGetWorkspaceMetaReq, req: CmdGetWorkspaceMetaReq,
) -> Result<WorkspaceMeta> { ) -> Result<WorkspaceMeta> {
let db = host.db(); let workspace = host.db().get_workspace(&req.workspace_id)?;
let workspace = db.get_workspace(&req.workspace_id)?; Ok(host.query_manager().with_tx(|tx| tx.ensure_workspace_meta(&workspace.id))?)
Ok(db.get_or_create_workspace_meta(&workspace.id)?)
} }
pub async fn cmd_delete_all_grpc_connections<H: Host>( pub async fn cmd_delete_all_grpc_connections<H: Host>(
host: H, host: H,
req: CmdDeleteAllGrpcConnectionsReq, req: CmdDeleteAllGrpcConnectionsReq,
) -> Result<()> { ) -> Result<()> {
Ok(host.db().delete_all_grpc_connections_for_request(&req.request_id, &host.update_source())?) Ok(host.query_manager().with_tx(|tx| {
tx.delete_all_grpc_connections_for_request(&req.request_id, &host.update_source())
})?)
} }
pub async fn cmd_delete_all_http_responses<H: Host>( pub async fn cmd_delete_all_http_responses<H: Host>(
host: H, host: H,
req: CmdDeleteAllHttpResponsesReq, req: CmdDeleteAllHttpResponsesReq,
) -> Result<()> { ) -> Result<()> {
host.db().delete_all_http_responses_for_request(&req.request_id, &host.update_source())?; host.query_manager().with_tx(|tx| {
tx.delete_all_http_responses_for_request(&req.request_id, &host.update_source())
})?;
Ok(()) Ok(())
} }
@@ -156,9 +164,9 @@ pub async fn cmd_ws_delete_connections<H: Host>(
host: H, host: H,
req: CmdWsDeleteConnectionsReq, req: CmdWsDeleteConnectionsReq,
) -> Result<()> { ) -> Result<()> {
Ok(host Ok(host.query_manager().with_tx(|tx| {
.db() tx.delete_all_websocket_connections_for_request(&req.request_id, &host.update_source())
.delete_all_websocket_connections_for_request(&req.request_id, &host.update_source())?) })?)
} }
pub async fn cmd_delete_send_history<H: Host>(host: H, req: CmdDeleteSendHistoryReq) -> Result<()> { pub async fn cmd_delete_send_history<H: Host>(host: H, req: CmdDeleteSendHistoryReq) -> Result<()> {
+23 -14
View File
@@ -118,6 +118,7 @@ impl Host for TestHost {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn writes_carry_the_client_id() { async fn writes_carry_the_client_id() {
let host = TestHost::new(); let host = TestHost::new();
host.drain_writes(); // the rows startup creates
let workspace = Workspace { name: "From a test".to_string(), ..Default::default() }; let workspace = Workspace { name: "From a test".to_string(), ..Default::default() };
let id = models_upsert(host.clone(), ModelsUpsertReq { model: AnyModel::Workspace(workspace) }) let id = models_upsert(host.clone(), ModelsUpsertReq { model: AnyModel::Workspace(workspace) })
@@ -406,8 +407,9 @@ async fn a_single_threaded_host_can_implement_the_trait() {
// shared code; only the callback came from the host. Rendering a real // shared code; only the callback came from the host. Rendering a real
// variable is what proves the chain was resolved rather than skipped. // variable is what proves the chain was resolved rather than skipped.
let environment = host let environment = host
.db() .query_manager()
.upsert_environment( .with_tx(|tx| {
tx.upsert_environment(
&Environment { &Environment {
workspace_id: id.clone(), workspace_id: id.clone(),
name: "Test env".to_string(), name: "Test env".to_string(),
@@ -421,6 +423,7 @@ async fn a_single_threaded_host_can_implement_the_trait() {
}, },
&host.update_source(), &host.update_source(),
) )
})
.expect("seed environment"); .expect("seed environment");
let rendered = cmd_render_template( let rendered = cmd_render_template(
@@ -459,14 +462,17 @@ async fn auth_values_are_rendered_before_the_host_sees_them() {
}; };
let workspace = host let workspace = host
.db() .query_manager()
.upsert_workspace( .with_tx(|tx| {
tx.upsert_workspace(
&Workspace { name: "Auth".to_string(), ..Default::default() }, &Workspace { name: "Auth".to_string(), ..Default::default() },
&host.update_source(), &host.update_source(),
) )
})
.expect("workspace"); .expect("workspace");
host.db() host.query_manager()
.upsert_environment( .with_tx(|tx| {
tx.upsert_environment(
&Environment { &Environment {
workspace_id: workspace.id.clone(), workspace_id: workspace.id.clone(),
name: "Env".to_string(), name: "Env".to_string(),
@@ -480,9 +486,9 @@ async fn auth_values_are_rendered_before_the_host_sees_them() {
}, },
&host.update_source(), &host.update_source(),
) )
})
.expect("environment"); .expect("environment");
let environment = let environment = host.db().list_environments(&workspace.id).expect("list").remove(0);
host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0);
let mut values = HashMap::new(); let mut values = HashMap::new();
values.insert("password".to_string(), JsonPrimitive::String("${[ token ]}".to_string())); values.insert("password".to_string(), JsonPrimitive::String("${[ token ]}".to_string()));
@@ -521,14 +527,17 @@ async fn template_function_values_are_rendered_before_the_host_sees_them() {
}; };
let workspace = host let workspace = host
.db() .query_manager()
.upsert_workspace( .with_tx(|tx| {
tx.upsert_workspace(
&Workspace { name: "Functions".to_string(), ..Default::default() }, &Workspace { name: "Functions".to_string(), ..Default::default() },
&host.update_source(), &host.update_source(),
) )
})
.expect("workspace"); .expect("workspace");
host.db() host.query_manager()
.upsert_environment( .with_tx(|tx| {
tx.upsert_environment(
&Environment { &Environment {
workspace_id: workspace.id.clone(), workspace_id: workspace.id.clone(),
name: "Env".to_string(), name: "Env".to_string(),
@@ -542,9 +551,9 @@ async fn template_function_values_are_rendered_before_the_host_sees_them() {
}, },
&host.update_source(), &host.update_source(),
) )
})
.expect("environment"); .expect("environment");
let environment = let environment = host.db().list_environments(&workspace.id).expect("list").remove(0);
host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0);
let mut values = HashMap::new(); let mut values = HashMap::new();
values.insert("token".to_string(), JsonPrimitive::String("${[1PASSWORD_TOKEN]}".to_string())); values.insert("token".to_string(), JsonPrimitive::String("${[1PASSWORD_TOKEN]}".to_string()));
+9 -7
View File
@@ -83,7 +83,7 @@ impl EncryptionManager {
let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| { let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| {
let workspace = tx.get_workspace(workspace_id)?; let workspace = tx.get_workspace(workspace_id)?;
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?; let workspace_meta = tx.ensure_workspace_meta(workspace_id)?;
tx.upsert_workspace( tx.upsert_workspace(
&Workspace { encryption_key_challenge, ..workspace }, &Workspace { encryption_key_challenge, ..workspace },
&UpdateSource::Background, &UpdateSource::Background,
@@ -103,7 +103,7 @@ impl EncryptionManager {
pub fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> { pub fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
let workspace_meta = let workspace_meta =
self.query_manager.connect().get_or_create_workspace_meta(workspace_id)?; self.query_manager.with_tx(|tx| tx.ensure_workspace_meta(workspace_id))?;
// Already exists // Already exists
if let Some(_) = workspace_meta.encryption_key { if let Some(_) = workspace_meta.encryption_key {
@@ -120,7 +120,7 @@ impl EncryptionManager {
self.query_manager.with_tx::<(), Error>(|tx| { self.query_manager.with_tx::<(), Error>(|tx| {
let workspace = tx.get_workspace(workspace_id)?; let workspace = tx.get_workspace(workspace_id)?;
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?; let workspace_meta = tx.ensure_workspace_meta(workspace_id)?;
// Clear encryption challenge on workspace // Clear encryption challenge on workspace
tx.upsert_workspace( tx.upsert_workspace(
@@ -152,10 +152,12 @@ impl EncryptionManager {
} }
}; };
let db = self.query_manager.connect(); let key = match self
let workspace_meta = db.get_or_create_workspace_meta(workspace_id)?; .query_manager
.connect()
let key = match workspace_meta.encryption_key { .get_workspace_meta(workspace_id)
.and_then(|m| m.encryption_key)
{
None => return Err(MissingWorkspaceKey), None => return Err(MissingWorkspaceKey),
Some(k) => k, Some(k) => k,
}; };
+19 -23
View File
@@ -7,7 +7,7 @@
use log::info; use log::info;
use std::path::PathBuf; use std::path::PathBuf;
use yaak_models::blob_manager::BlobManager; use yaak_models::blob_manager::BlobManager;
use yaak_models::client_db::ClientDb; use yaak_models::client_db::WriteDb;
use yaak_models::error::Result; use yaak_models::error::Result;
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1; const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
@@ -44,7 +44,8 @@ impl Host {
} }
/// Run once after the database is open, before the host answers anything. /// Run once after the database is open, before the host answers anything.
pub fn on_launch(host: &Host, db: &ClientDb, blobs: &BlobManager) -> Result<()> { /// Takes the write handle: a launch closes what the last session left open.
pub fn on_launch(host: &Host, db: &WriteDb, blobs: &BlobManager) -> Result<()> {
db.prune_model_changes_older_than_hours(MODEL_CHANGES_RETENTION_HOURS)?; db.prune_model_changes_older_than_hours(MODEL_CHANGES_RETENTION_HOURS)?;
if host.role == Role::Owner { if host.role == Role::Owner {
@@ -77,23 +78,19 @@ mod tests {
#[test] #[test]
fn only_the_owner_closes_what_the_last_session_left_open() { fn only_the_owner_closes_what_the_last_session_left_open() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect();
let source = &UpdateSource::Background; let source = &UpdateSource::Background;
let workspace = db let pending = query_manager
.upsert_workspace( .with_tx(|db| {
let workspace = db.upsert_workspace(
&Workspace { name: "Hooks".to_string(), ..Default::default() }, &Workspace { name: "Hooks".to_string(), ..Default::default() },
source, source,
) )?;
.unwrap(); let request = db.upsert_http_request(
let request = db
.upsert_http_request(
&HttpRequest { workspace_id: workspace.id.clone(), ..Default::default() }, &HttpRequest { workspace_id: workspace.id.clone(), ..Default::default() },
source, source,
) )?;
.unwrap(); db.upsert_http_response(
let pending = db
.upsert_http_response(
&HttpResponse { &HttpResponse {
request_id: request.id.clone(), request_id: request.id.clone(),
workspace_id: workspace.id.clone(), workspace_id: workspace.id.clone(),
@@ -103,27 +100,26 @@ mod tests {
source, source,
&blob_manager, &blob_manager,
) )
})
.unwrap(); .unwrap();
on_launch(&Host::guest(), &db, &blob_manager).unwrap(); query_manager.with_tx(|db| on_launch(&Host::guest(), db, &blob_manager)).unwrap();
let response = db.get_http_response(&pending.id).unwrap(); let response = query_manager.connect().get_http_response(&pending.id).unwrap();
assert!(matches!(response.state, HttpResponseState::Connected)); assert!(matches!(response.state, HttpResponseState::Connected));
on_launch(&Host::owner(), &db, &blob_manager).unwrap(); query_manager.with_tx(|db| on_launch(&Host::owner(), db, &blob_manager)).unwrap();
let response = db.get_http_response(&pending.id).unwrap(); let response = query_manager.connect().get_http_response(&pending.id).unwrap();
assert!(matches!(response.state, HttpResponseState::Closed)); assert!(matches!(response.state, HttpResponseState::Closed));
} }
#[test] #[test]
fn owner_without_a_filesystem_still_sweeps_blobs() { fn owner_without_a_filesystem_still_sweeps_blobs() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); blob_manager
{ .with_tx(|b| b.insert_chunk(&BodyChunk::new("rs_gone", 0, b"dead".to_vec())))
let blob_ctx = blob_manager.connect(); .unwrap();
blob_ctx.insert_chunk(&BodyChunk::new("rs_gone", 0, b"dead".to_vec())).unwrap();
}
on_launch(&Host::owner(), &db, &blob_manager).unwrap(); query_manager.with_tx(|db| on_launch(&Host::owner(), db, &blob_manager)).unwrap();
assert!(!blob_manager.connect().body_exists("rs_gone").unwrap()); assert!(!blob_manager.connect().body_exists("rs_gone").unwrap());
} }
+164 -109
View File
@@ -1,9 +1,11 @@
use crate::error::Error::GenericError;
use crate::error::Result; use crate::error::Result;
use crate::util::generate_prefixed_id; use crate::util::generate_prefixed_id;
use include_dir::{Dir, include_dir}; use include_dir::{Dir, include_dir};
use log::{debug, info}; use log::{debug, info};
use rusqlite::{OptionalExtension, params}; use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
use yaak_database::{SqliteConn, SqlitePool}; use std::ops::Deref;
use yaak_database::{ConnectionOrTx, SqlitePool};
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations"); static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations");
@@ -22,41 +24,64 @@ impl BodyChunk {
} }
} }
/// Manages the blob database connection pool. /// Manages the blob database: a reader pool and a single writer, for the
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the /// same reason as [`crate::query_manager::QueryManager`].
// Pools are internally synchronized — don't wrap them in a Mutex. A Mutex held across the
// blocking `get()` serializes every blob access behind the slowest waiter, freezing the // blocking `get()` serializes every blob access behind the slowest waiter, freezing the
// whole app whenever the pool is exhausted. // whole app whenever the pool is exhausted.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BlobManager { pub struct BlobManager {
pool: SqlitePool, readers: SqlitePool,
writer: SqlitePool,
} }
impl BlobManager { impl BlobManager {
pub fn new(pool: SqlitePool) -> Self { /// `writer` must be a pool with a single connection.
Self { pool } pub fn new(readers: SqlitePool, writer: SqlitePool) -> Self {
Self { readers, writer }
} }
pub fn connect(&self) -> BlobContext { /// A read handle from the reader pool.
let conn = self.pool.get().expect("Failed to get blob DB connection from pool"); pub fn connect(&self) -> BlobContext<'_> {
BlobContext { conn } let conn = self.readers.get().expect("Failed to get blob DB connection from pool");
BlobContext { conn: ConnectionOrTx::Connection(conn) }
}
/// Run `func` in a transaction on the writer connection.
pub fn with_tx<T, E>(
&self,
func: impl FnOnce(&BlobWriter) -> std::result::Result<T, E>,
) -> std::result::Result<T, E>
where
E: From<crate::error::Error>,
{
let conn = self.writer.get().map_err(crate::error::Error::SqlPoolError)?;
let tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)
.map_err(crate::error::Error::SqlError)?;
let writer = BlobWriter { ctx: BlobContext { conn: ConnectionOrTx::Transaction(&tx) } };
match func(&writer) {
Ok(val) => {
tx.commit().map_err(|e| {
GenericError(format!("Failed to commit blob transaction {e:?}"))
})?;
Ok(val)
}
Err(e) => {
tx.rollback().map_err(|e| {
GenericError(format!("Failed to rollback blob transaction {e:?}"))
})?;
Err(e)
}
}
} }
} }
/// Context for blob database operations. /// Read handle for the blob database.
pub struct BlobContext { pub struct BlobContext<'a> {
conn: SqliteConn, conn: ConnectionOrTx<'a>,
}
impl BlobContext {
/// Insert a single chunk.
pub fn insert_chunk(&self, chunk: &BodyChunk) -> Result<()> {
self.conn.execute(
"INSERT INTO body_chunks (id, body_id, chunk_index, data) VALUES (?1, ?2, ?3, ?4)",
params![chunk.id, chunk.body_id, chunk.chunk_index, chunk.data],
)?;
Ok(())
} }
impl<'a> BlobContext<'a> {
/// Get all chunks for a body, ordered by chunk_index. /// Get all chunks for a body, ordered by chunk_index.
pub fn get_chunks(&self, body_id: &str) -> Result<Vec<BodyChunk>> { pub fn get_chunks(&self, body_id: &str) -> Result<Vec<BodyChunk>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
@@ -87,25 +112,11 @@ impl BlobContext {
Ok(ids) Ok(ids)
} }
/// Delete all chunks for a body.
pub fn delete_chunks(&self, body_id: &str) -> Result<()> {
self.conn.execute("DELETE FROM body_chunks WHERE body_id = ?1", params![body_id])?;
Ok(())
}
/// Delete all chunks matching a body_id prefix (e.g., "rs_abc123.%" to delete all bodies for a response).
pub fn delete_chunks_like(&self, body_id_prefix: &str) -> Result<()> {
self.conn
.execute("DELETE FROM body_chunks WHERE body_id LIKE ?1", params![body_id_prefix])?;
Ok(())
}
}
/// Get total size of a body without loading data. /// Get total size of a body without loading data.
impl BlobContext {
pub fn get_body_size(&self, body_id: &str) -> Result<usize> { pub fn get_body_size(&self, body_id: &str) -> Result<usize> {
let size: i64 = self let size: i64 = self
.conn .conn
.resolve()
.query_row( .query_row(
"SELECT COALESCE(SUM(LENGTH(data)), 0) FROM body_chunks WHERE body_id = ?1", "SELECT COALESCE(SUM(LENGTH(data)), 0) FROM body_chunks WHERE body_id = ?1",
params![body_id], params![body_id],
@@ -119,6 +130,7 @@ impl BlobContext {
pub fn body_exists(&self, body_id: &str) -> Result<bool> { pub fn body_exists(&self, body_id: &str) -> Result<bool> {
let count: i64 = self let count: i64 = self
.conn .conn
.resolve()
.query_row( .query_row(
"SELECT COUNT(*) FROM body_chunks WHERE body_id = ?1", "SELECT COUNT(*) FROM body_chunks WHERE body_id = ?1",
params![body_id], params![body_id],
@@ -129,6 +141,44 @@ impl BlobContext {
} }
} }
/// Write handle for the blob database. Derefs to [`BlobContext`] for reads.
pub struct BlobWriter<'a> {
ctx: BlobContext<'a>,
}
impl<'a> Deref for BlobWriter<'a> {
type Target = BlobContext<'a>;
fn deref(&self) -> &BlobContext<'a> {
&self.ctx
}
}
impl<'a> BlobWriter<'a> {
/// Insert a single chunk.
pub fn insert_chunk(&self, chunk: &BodyChunk) -> Result<()> {
self.conn.execute(
"INSERT INTO body_chunks (id, body_id, chunk_index, data) VALUES (?1, ?2, ?3, ?4)",
params![chunk.id, chunk.body_id, chunk.chunk_index, chunk.data],
)?;
Ok(())
}
/// Delete all chunks for a body.
pub fn delete_chunks(&self, body_id: &str) -> Result<()> {
self.conn.execute("DELETE FROM body_chunks WHERE body_id = ?1", params![body_id])?;
Ok(())
}
/// Delete all chunks matching a body_id prefix (e.g., "rs_abc123.%" to delete all
/// bodies for a response).
pub fn delete_chunks_like(&self, body_id_prefix: &str) -> Result<()> {
self.conn
.execute("DELETE FROM body_chunks WHERE body_id LIKE ?1", params![body_id_prefix])?;
Ok(())
}
}
/// Run migrations for the blob database. /// Run migrations for the blob database.
pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> { pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> {
info!("Running blob database migrations"); info!("Running blob database migrations");
@@ -196,28 +246,39 @@ pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::error::Error;
fn create_test_pool() -> SqlitePool { fn create_test_manager() -> BlobManager {
let manager = r2d2_sqlite::SqliteConnectionManager::memory(); let manager = r2d2_sqlite::SqliteConnectionManager::memory();
let pool = r2d2::Pool::builder().max_size(1).build(manager).unwrap(); let pool = r2d2::Pool::builder().max_size(1).build(manager).unwrap();
migrate_blob_db(&pool).unwrap(); migrate_blob_db(&pool).unwrap();
pool BlobManager::new(pool.clone(), pool)
}
fn insert(manager: &BlobManager, chunks: &[BodyChunk]) {
manager
.with_tx(|b| {
for c in chunks {
b.insert_chunk(c)?;
}
Ok::<_, Error>(())
})
.unwrap();
} }
#[test] #[test]
fn test_insert_and_get_chunks() { fn test_insert_and_get_chunks() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
let body_id = "rs_test123.request"; let body_id = "rs_test123.request";
let chunk1 = BodyChunk::new(body_id, 0, b"Hello, ".to_vec()); insert(
let chunk2 = BodyChunk::new(body_id, 1, b"World!".to_vec()); &manager,
&[
BodyChunk::new(body_id, 0, b"Hello, ".to_vec()),
BodyChunk::new(body_id, 1, b"World!".to_vec()),
],
);
ctx.insert_chunk(&chunk1).unwrap(); let chunks = manager.connect().get_chunks(body_id).unwrap();
ctx.insert_chunk(&chunk2).unwrap();
let chunks = ctx.get_chunks(body_id).unwrap();
assert_eq!(chunks.len(), 2); assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].chunk_index, 0); assert_eq!(chunks[0].chunk_index, 0);
assert_eq!(chunks[0].data, b"Hello, "); assert_eq!(chunks[0].data, b"Hello, ");
@@ -227,18 +288,19 @@ mod tests {
#[test] #[test]
fn test_get_chunks_ordered_by_index() { fn test_get_chunks_ordered_by_index() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
let body_id = "rs_test123.request"; let body_id = "rs_test123.request";
// Insert out of order // Insert out of order
ctx.insert_chunk(&BodyChunk::new(body_id, 2, b"C".to_vec())).unwrap(); insert(
ctx.insert_chunk(&BodyChunk::new(body_id, 0, b"A".to_vec())).unwrap(); &manager,
ctx.insert_chunk(&BodyChunk::new(body_id, 1, b"B".to_vec())).unwrap(); &[
BodyChunk::new(body_id, 2, b"C".to_vec()),
BodyChunk::new(body_id, 0, b"A".to_vec()),
BodyChunk::new(body_id, 1, b"B".to_vec()),
],
);
let chunks = ctx.get_chunks(body_id).unwrap(); let chunks = manager.connect().get_chunks(body_id).unwrap();
assert_eq!(chunks.len(), 3); assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].data, b"A"); assert_eq!(chunks[0].data, b"A");
assert_eq!(chunks[1].data, b"B"); assert_eq!(chunks[1].data, b"B");
@@ -247,89 +309,85 @@ mod tests {
#[test] #[test]
fn test_delete_chunks() { fn test_delete_chunks() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
let body_id = "rs_test123.request"; let body_id = "rs_test123.request";
ctx.insert_chunk(&BodyChunk::new(body_id, 0, b"data".to_vec())).unwrap(); insert(&manager, &[BodyChunk::new(body_id, 0, b"data".to_vec())]);
assert!(manager.connect().body_exists(body_id).unwrap());
assert!(ctx.body_exists(body_id).unwrap()); manager.with_tx(|b| b.delete_chunks(body_id)).unwrap();
ctx.delete_chunks(body_id).unwrap();
let ctx = manager.connect();
assert!(!ctx.body_exists(body_id).unwrap()); assert!(!ctx.body_exists(body_id).unwrap());
assert_eq!(ctx.get_chunks(body_id).unwrap().len(), 0); assert_eq!(ctx.get_chunks(body_id).unwrap().len(), 0);
} }
#[test] #[test]
fn test_delete_chunks_like() { fn test_delete_chunks_like() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
// Insert chunks for same response but different body types // Insert chunks for same response but different body types
ctx.insert_chunk(&BodyChunk::new("rs_abc.request", 0, b"req".to_vec())).unwrap(); insert(
ctx.insert_chunk(&BodyChunk::new("rs_abc.response", 0, b"resp".to_vec())).unwrap(); &manager,
ctx.insert_chunk(&BodyChunk::new("rs_other.request", 0, b"other".to_vec())).unwrap(); &[
BodyChunk::new("rs_abc.request", 0, b"req".to_vec()),
BodyChunk::new("rs_abc.response", 0, b"resp".to_vec()),
BodyChunk::new("rs_other.request", 0, b"other".to_vec()),
],
);
// Delete all bodies for rs_abc // Delete all bodies for rs_abc
ctx.delete_chunks_like("rs_abc.%").unwrap(); manager.with_tx(|b| b.delete_chunks_like("rs_abc.%")).unwrap();
// rs_abc bodies should be gone let ctx = manager.connect();
assert!(!ctx.body_exists("rs_abc.request").unwrap()); assert!(!ctx.body_exists("rs_abc.request").unwrap());
assert!(!ctx.body_exists("rs_abc.response").unwrap()); assert!(!ctx.body_exists("rs_abc.response").unwrap());
// rs_other should still exist
assert!(ctx.body_exists("rs_other.request").unwrap()); assert!(ctx.body_exists("rs_other.request").unwrap());
} }
#[test] #[test]
fn test_get_body_size() { fn test_get_body_size() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
let body_id = "rs_test123.request"; let body_id = "rs_test123.request";
ctx.insert_chunk(&BodyChunk::new(body_id, 0, b"Hello".to_vec())).unwrap(); insert(
ctx.insert_chunk(&BodyChunk::new(body_id, 1, b"World".to_vec())).unwrap(); &manager,
&[
BodyChunk::new(body_id, 0, b"Hello".to_vec()),
BodyChunk::new(body_id, 1, b"World".to_vec()),
],
);
let size = ctx.get_body_size(body_id).unwrap(); let size = manager.connect().get_body_size(body_id).unwrap();
assert_eq!(size, 10); // "Hello" + "World" = 10 bytes assert_eq!(size, 10); // "Hello" + "World" = 10 bytes
} }
#[test] #[test]
fn test_get_body_size_empty() { fn test_get_body_size_empty() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool); let size = manager.connect().get_body_size("nonexistent").unwrap();
let ctx = manager.connect();
let size = ctx.get_body_size("nonexistent").unwrap();
assert_eq!(size, 0); assert_eq!(size, 0);
} }
#[test] #[test]
fn test_body_exists() { fn test_body_exists() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool); assert!(!manager.connect().body_exists("rs_test.request").unwrap());
let ctx = manager.connect();
assert!(!ctx.body_exists("rs_test.request").unwrap()); insert(&manager, &[BodyChunk::new("rs_test.request", 0, b"data".to_vec())]);
ctx.insert_chunk(&BodyChunk::new("rs_test.request", 0, b"data".to_vec())).unwrap(); assert!(manager.connect().body_exists("rs_test.request").unwrap());
assert!(ctx.body_exists("rs_test.request").unwrap());
} }
#[test] #[test]
fn test_multiple_bodies_isolated() { fn test_multiple_bodies_isolated() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool); insert(
&manager,
&[
BodyChunk::new("body1", 0, b"data1".to_vec()),
BodyChunk::new("body2", 0, b"data2".to_vec()),
],
);
let ctx = manager.connect(); let ctx = manager.connect();
ctx.insert_chunk(&BodyChunk::new("body1", 0, b"data1".to_vec())).unwrap();
ctx.insert_chunk(&BodyChunk::new("body2", 0, b"data2".to_vec())).unwrap();
let chunks1 = ctx.get_chunks("body1").unwrap(); let chunks1 = ctx.get_chunks("body1").unwrap();
let chunks2 = ctx.get_chunks("body2").unwrap(); let chunks2 = ctx.get_chunks("body2").unwrap();
@@ -341,16 +399,13 @@ mod tests {
#[test] #[test]
fn test_large_chunk() { fn test_large_chunk() {
let pool = create_test_pool(); let manager = create_test_manager();
let manager = BlobManager::new(pool);
let ctx = manager.connect();
// 1MB chunk // 1MB chunk
let large_data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect(); let large_data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
let body_id = "rs_large.request"; let body_id = "rs_large.request";
insert(&manager, &[BodyChunk::new(body_id, 0, large_data.clone())]);
ctx.insert_chunk(&BodyChunk::new(body_id, 0, large_data.clone())).unwrap(); let ctx = manager.connect();
let chunks = ctx.get_chunks(body_id).unwrap(); let chunks = ctx.get_chunks(body_id).unwrap();
assert_eq!(chunks.len(), 1); assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].data, large_data); assert_eq!(chunks[0].data, large_data);
+45 -9
View File
@@ -3,18 +3,25 @@ use crate::models::{AnyModel, UpsertModelInfo};
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource}; use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
use rusqlite::params; use rusqlite::params;
use sea_query::{IntoColumnRef, IntoIden, SimpleExpr}; use sea_query::{IntoColumnRef, IntoIden, SimpleExpr};
use std::cell::RefCell;
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Deref;
use std::sync::mpsc; use std::sync::mpsc;
use yaak_database::DbContext; use yaak_database::DbContext;
/// A read handle. Comes from the reader pool and can only query.
///
/// Anything that changes a row lives on [`WriteDb`], which is only ever handed
/// out inside a transaction on the single writer connection. That split is
/// what keeps the pool from filling with writers waiting on each other: there
/// is one writer, so there is never a second one to wait for.
pub struct ClientDb<'a> { pub struct ClientDb<'a> {
pub(crate) ctx: DbContext<'a>, pub(crate) ctx: DbContext<'a>,
pub(crate) events_tx: mpsc::Sender<ModelPayload>,
} }
impl<'a> ClientDb<'a> { impl<'a> ClientDb<'a> {
pub fn new(ctx: DbContext<'a>, events_tx: mpsc::Sender<ModelPayload>) -> Self { pub fn new(ctx: DbContext<'a>) -> Self {
Self { ctx, events_tx } Self { ctx }
} }
/// Access the underlying connection for custom queries. /// Access the underlying connection for custom queries.
@@ -22,8 +29,6 @@ impl<'a> ClientDb<'a> {
self.ctx.conn() self.ctx.conn()
} }
// --- Read delegates (thin wrappers over DbContext) ---
pub(crate) fn find_one<M>( pub(crate) fn find_one<M>(
&self, &self,
col: impl IntoColumnRef + IntoIden + Clone, col: impl IntoColumnRef + IntoIden + Clone,
@@ -64,6 +69,39 @@ impl<'a> ClientDb<'a> {
{ {
Ok(self.ctx.find_many(col, value, limit)?) Ok(self.ctx.find_many(col, value, limit)?)
} }
}
/// A write handle: a [`ClientDb`] on the writer connection, inside a
/// transaction, that can also change rows. Derefs to [`ClientDb`] so every
/// query is available while writing, and reads inside the transaction see
/// its own uncommitted writes.
///
/// Model events are held back until the transaction commits; a rollback
/// discards them along with the rows.
pub struct WriteDb<'a> {
db: ClientDb<'a>,
events_tx: mpsc::Sender<ModelPayload>,
pending_events: RefCell<Vec<ModelPayload>>,
}
impl<'a> Deref for WriteDb<'a> {
type Target = ClientDb<'a>;
fn deref(&self) -> &ClientDb<'a> {
&self.db
}
}
impl<'a> WriteDb<'a> {
pub fn new(ctx: DbContext<'a>, events_tx: mpsc::Sender<ModelPayload>) -> Self {
Self { db: ClientDb::new(ctx), events_tx, pending_events: RefCell::new(Vec::new()) }
}
/// The events for everything written so far, to send once the
/// transaction has committed.
pub(crate) fn into_events(self) -> Vec<ModelPayload> {
self.pending_events.into_inner()
}
/// Bulk-delete all rows matching a column value WITHOUT recording model /// Bulk-delete all rows matching a column value WITHOUT recording model
/// changes or emitting events. Only use for cascades whose deletion is /// changes or emitting events. Only use for cascades whose deletion is
@@ -80,8 +118,6 @@ impl<'a> ClientDb<'a> {
Ok(self.ctx.delete_many::<M>(col, value)?) Ok(self.ctx.delete_many::<M>(col, value)?)
} }
// --- Write operations (with event recording) ---
pub(crate) fn upsert<M>(&self, model: &M, source: &UpdateSource) -> Result<M> pub(crate) fn upsert<M>(&self, model: &M, source: &UpdateSource) -> Result<M>
where where
M: Into<AnyModel> + UpsertModelInfo + Clone, M: Into<AnyModel> + UpsertModelInfo + Clone,
@@ -95,7 +131,7 @@ impl<'a> ClientDb<'a> {
}; };
self.record_model_change(&payload)?; self.record_model_change(&payload)?;
let _ = self.events_tx.send(payload); self.pending_events.borrow_mut().push(payload);
Ok(m) Ok(m)
} }
@@ -113,7 +149,7 @@ impl<'a> ClientDb<'a> {
}; };
self.record_model_change(&payload)?; self.record_model_change(&payload)?;
let _ = self.events_tx.send(payload); self.pending_events.borrow_mut().push(payload);
Ok(m.clone()) Ok(m.clone())
} }
+49 -12
View File
@@ -64,6 +64,17 @@ mod open {
.map_err(|e| Error::Database(e.to_string())) .map_err(|e| Error::Database(e.to_string()))
} }
/// `(readers, writer)` over one file: a pool of `max_size` readers and a
/// pool of exactly one writer.
pub fn file_pools(
path: impl Into<PathBuf>,
max_size: u32,
min_idle: u32,
) -> Result<(SqlitePool, SqlitePool)> {
let path: PathBuf = path.into();
Ok((file_pool(&path, max_size, min_idle)?, file_pool(&path, 1, 1)?))
}
pub fn memory_pool() -> Result<SqlitePool> { pub fn memory_pool() -> Result<SqlitePool> {
let manager = SqliteConnectionManager::memory().with_init(|c| init_connection(c)); let manager = SqliteConnectionManager::memory().with_init(|c| init_connection(c));
// In-memory DB doesn't support multiple connections // In-memory DB doesn't support multiple connections
@@ -90,6 +101,16 @@ mod open {
Ok(SqlitePool::single(conn)) Ok(SqlitePool::single(conn))
} }
/// One connection is all a browser VFS allows, so it reads and writes.
pub fn file_pools(
path: impl Into<PathBuf>,
max_size: u32,
min_idle: u32,
) -> Result<(SqlitePool, SqlitePool)> {
let pool = file_pool(path, max_size, min_idle)?;
Ok((pool.clone(), pool))
}
pub fn memory_pool() -> Result<SqlitePool> { pub fn memory_pool() -> Result<SqlitePool> {
let conn = Connection::open_in_memory()?; let conn = Connection::open_in_memory()?;
init_connection(&conn)?; init_connection(&conn)?;
@@ -108,27 +129,33 @@ pub fn init_standalone(
let db_path = db_path.as_ref(); let db_path = db_path.as_ref();
let blob_path = blob_path.as_ref(); let blob_path = blob_path.as_ref();
// Main database pool. Sized for concurrent in-flight queries, not concurrent app // Each database gets a reader pool and a one-connection writer pool; see
// features — connections are held per-statement, so even heavy fan-out (e.g. many // `QueryManager` for why. Reader pools are sized for concurrent in-flight
// gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections // queries, not concurrent app features — connections are held per-statement,
// hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit. // so even heavy fan-out (e.g. many gRPC streams) only needs a handful at once.
// Keep them modest: WAL connections hold ~3 file descriptors each, and macOS
// GUI apps get a 256 fd soft limit.
info!("Initializing app database {db_path:?}"); info!("Initializing app database {db_path:?}");
let pool = open::file_pool(db_path, 20, 2)?; let (readers, writer) = open::file_pools(db_path, 20, 2)?;
migrate_db(&pool)?; migrate_db(&writer)?;
info!("Initializing blobs database {blob_path:?}"); info!("Initializing blobs database {blob_path:?}");
let blob_pool = open::file_pool(blob_path, 10, 1)?; let (blob_readers, blob_writer) = open::file_pools(blob_path, 10, 1)?;
migrate_blob_db(&blob_pool)?; migrate_blob_db(&blob_writer)?;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let query_manager = QueryManager::new(pool, tx); let query_manager = QueryManager::new(readers, writer, tx);
let blob_manager = BlobManager::new(blob_pool); let blob_manager = BlobManager::new(blob_readers, blob_writer);
bootstrap(&query_manager)?;
Ok((query_manager, blob_manager, rx)) Ok((query_manager, blob_manager, rx))
} }
/// Initialize the database managers with in-memory SQLite databases. /// Initialize the database managers with in-memory SQLite databases.
/// Useful for testing and CI environments. /// Useful for testing and CI environments.
///
/// An in-memory database is private to its connection, so the one connection
/// is both the reader pool and the writer.
pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<ModelPayload>)> { pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<ModelPayload>)> {
let pool = open::memory_pool()?; let pool = open::memory_pool()?;
migrate_db(&pool)?; migrate_db(&pool)?;
@@ -137,8 +164,18 @@ pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<Mod
migrate_blob_db(&blob_pool)?; migrate_blob_db(&blob_pool)?;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let query_manager = QueryManager::new(pool, tx); let query_manager = QueryManager::new(pool.clone(), pool, tx);
let blob_manager = BlobManager::new(blob_pool); let blob_manager = BlobManager::new(blob_pool.clone(), blob_pool);
bootstrap(&query_manager)?;
Ok((query_manager, blob_manager, rx)) Ok((query_manager, blob_manager, rx))
} }
/// The rows every client assumes exist: settings and at least one workspace.
fn bootstrap(query_manager: &QueryManager) -> Result<()> {
query_manager.with_tx(|tx| {
tx.ensure_settings()?;
tx.ensure_default_workspace()?;
Ok(())
})
}
+4 -4
View File
@@ -8,14 +8,14 @@
//! caller is a desktop window or an HTTP request. //! caller is a desktop window or an HTTP request.
use crate::blob_manager::BlobManager; use crate::blob_manager::BlobManager;
use crate::client_db::ClientDb; use crate::client_db::WriteDb;
use crate::error::Error::GenericError; use crate::error::Error::GenericError;
use crate::error::Result; use crate::error::Result;
use crate::models::AnyModel; use crate::models::AnyModel;
use crate::util::UpdateSource; use crate::util::UpdateSource;
pub fn upsert_model( pub fn upsert_model(
db: &ClientDb, db: &WriteDb,
blobs: &BlobManager, blobs: &BlobManager,
model: AnyModel, model: AnyModel,
source: &UpdateSource, source: &UpdateSource,
@@ -41,7 +41,7 @@ pub fn upsert_model(
/// Deletes cascade, so callers run this inside a transaction. /// Deletes cascade, so callers run this inside a transaction.
pub fn delete_model( pub fn delete_model(
tx: &ClientDb, tx: &WriteDb,
blobs: &BlobManager, blobs: &BlobManager,
model: AnyModel, model: AnyModel,
source: &UpdateSource, source: &UpdateSource,
@@ -69,7 +69,7 @@ pub fn delete_model(
/// The model is re-read from the database rather than taken from the caller, so /// The model is re-read from the database rather than taken from the caller, so
/// a duplicate never comes from a stale frontend snapshot. /// a duplicate never comes from a stale frontend snapshot.
pub fn duplicate_model( pub fn duplicate_model(
tx: &ClientDb, tx: &WriteDb,
model_type: &str, model_type: &str,
model_id: &str, model_id: &str,
source: &UpdateSource, source: &UpdateSource,
+2 -2
View File
@@ -1,10 +1,10 @@
use crate::client_db::ClientDb; use crate::client_db::WriteDb;
use crate::error::Result; use crate::error::Result;
use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace}; use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace};
use crate::util::{BatchUpsertResult, UpdateSource}; use crate::util::{BatchUpsertResult, UpdateSource};
use log::info; use log::info;
impl<'a> ClientDb<'a> { impl<'a> WriteDb<'a> {
pub fn batch_upsert( pub fn batch_upsert(
&self, &self,
workspaces: Vec<Workspace>, workspaces: Vec<Workspace>,
+10 -6
View File
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{CookieJar, CookieJarIden}; use crate::models::{CookieJar, CookieJarIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -9,18 +9,22 @@ impl<'a> ClientDb<'a> {
} }
pub fn list_cookie_jars(&self, workspace_id: &str) -> Result<Vec<CookieJar>> { pub fn list_cookie_jars(&self, workspace_id: &str) -> Result<Vec<CookieJar>> {
let mut cookie_jars = self.find_many(CookieJarIden::WorkspaceId, workspace_id, None)?; self.find_many(CookieJarIden::WorkspaceId, workspace_id, None)
}
}
if cookie_jars.is_empty() { impl<'a> WriteDb<'a> {
/// A workspace with no cookie jar gets a default one.
pub fn ensure_default_cookie_jar(&self, workspace_id: &str) -> Result<()> {
if self.list_cookie_jars(workspace_id)?.is_empty() {
let jar = CookieJar { let jar = CookieJar {
name: "Default".to_string(), name: "Default".to_string(),
workspace_id: workspace_id.to_string(), workspace_id: workspace_id.to_string(),
..Default::default() ..Default::default()
}; };
cookie_jars.push(self.upsert_cookie_jar(&jar, &UpdateSource::Background)?); self.upsert_cookie_jar(&jar, &UpdateSource::Background)?;
} }
Ok(())
Ok(cookie_jars)
} }
pub fn delete_cookie_jar( pub fn delete_cookie_jar(
+80 -78
View File
@@ -1,5 +1,5 @@
use super::conflict_free_name; use super::conflict_free_name;
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Error::{MissingBaseEnvironment, MultipleBaseEnvironments}; use crate::error::Error::{MissingBaseEnvironment, MultipleBaseEnvironments};
use crate::error::Result; use crate::error::Result;
use crate::models::{Environment, EnvironmentIden, EnvironmentVariable}; use crate::models::{Environment, EnvironmentIden, EnvironmentVariable};
@@ -20,7 +20,7 @@ impl<'a> ClientDb<'a> {
} }
pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> { pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> {
let environments = self.list_environments_ensure_base(workspace_id)?; let environments = self.list_environments(workspace_id)?;
let base_environments = environments let base_environments = environments
.into_iter() .into_iter()
.filter(|e| e.parent_model == "workspace") .filter(|e| e.parent_model == "workspace")
@@ -30,19 +30,85 @@ impl<'a> ClientDb<'a> {
return Err(MultipleBaseEnvironments(workspace_id.to_string())); return Err(MultipleBaseEnvironments(workspace_id.to_string()));
} }
Ok(base_environments.first().cloned().ok_or( Ok(base_environments
// Should never happen because one should be created above if it does not exist .first()
MissingBaseEnvironment(workspace_id.to_string()), .cloned()
)?) .ok_or(MissingBaseEnvironment(workspace_id.to_string()))?)
} }
/// Lists environments and will create a base environment if one doesn't exist pub fn list_environments(&self, workspace_id: &str) -> Result<Vec<Environment>> {
pub fn list_environments_ensure_base(&self, workspace_id: &str) -> Result<Vec<Environment>> { Ok(self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?)
let mut environments = self.list_environments_dangerous(workspace_id)?; }
let base_environment = environments.iter().find(|e| e.parent_model == "workspace"); /// Find other environments with the same parent folder
fn list_duplicate_folder_environments(&self, environment: &Environment) -> Vec<Environment> {
if environment.parent_model != "folder" {
return Vec::new();
}
if let None = base_environment { self.list_environments(&environment.workspace_id)
.unwrap_or_default()
.into_iter()
.filter(|e| {
e.id != environment.id
&& e.parent_model == "folder"
&& e.parent_id == environment.parent_id
})
.collect()
}
pub fn resolve_environments(
&self,
workspace_id: &str,
folder_id: Option<&str>,
active_environment_id: Option<&str>,
) -> Result<Vec<Environment>> {
let mut environments = Vec::new();
if let Some(folder_id) = folder_id {
let folder = self.get_folder(folder_id)?;
// Add current folder's environment
if let Some(e) = self.get_environment_by_folder_id(folder_id)? {
environments.push(e);
};
// Recurse up
let ancestors = self.resolve_environments(
workspace_id,
folder.folder_id.as_deref(),
active_environment_id,
)?;
environments.extend(ancestors);
} else {
// Add active and base environments
if let Some(id) = active_environment_id {
if let Ok(e) = self.get_environment(&id) {
// Add active sub environment
environments.push(e);
};
};
// Add the base environment. A workspace that has never been
// opened has none yet; it simply contributes no variables.
match self.get_base_environment(workspace_id) {
Ok(e) => environments.push(e),
Err(MissingBaseEnvironment(_)) => {}
Err(e) => return Err(e),
}
}
Ok(environments)
}
}
impl<'a> WriteDb<'a> {
/// The workspace's base environment, created if it does not exist.
pub fn ensure_base_environment(&self, workspace_id: &str) -> Result<Environment> {
match self.get_base_environment(workspace_id) {
Err(MissingBaseEnvironment(_)) => {}
other => return other,
}
let e = self.upsert_environment( let e = self.upsert_environment(
&Environment { &Environment {
workspace_id: workspace_id.to_string(), workspace_id: workspace_id.to_string(),
@@ -53,15 +119,7 @@ impl<'a> ClientDb<'a> {
&UpdateSource::Background, &UpdateSource::Background,
)?; )?;
info!("Created base environment {} for {workspace_id}", e.id); info!("Created base environment {} for {workspace_id}", e.id);
environments.push(e); Ok(e)
}
Ok(environments)
}
/// List environments for a workspace. Prefer list_environments_ensure_base()
fn list_environments_dangerous(&self, workspace_id: &str) -> Result<Vec<Environment>> {
Ok(self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?)
} }
pub fn delete_environment( pub fn delete_environment(
@@ -72,7 +130,7 @@ impl<'a> ClientDb<'a> {
let deleted_environment = self.delete(environment, source)?; let deleted_environment = self.delete(environment, source)?;
// Recreate the base environment if we happened to delete it // Recreate the base environment if we happened to delete it
self.list_environments_ensure_base(&environment.workspace_id)?; self.ensure_base_environment(&environment.workspace_id)?;
Ok(deleted_environment) Ok(deleted_environment)
} }
@@ -90,7 +148,7 @@ impl<'a> ClientDb<'a> {
let mut environment = environment.clone(); let mut environment = environment.clone();
environment.id = "".to_string(); environment.id = "".to_string();
let sibling_names = self let sibling_names = self
.list_environments_dangerous(&environment.workspace_id)? .list_environments(&environment.workspace_id)?
.into_iter() .into_iter()
.map(|e| e.name) .map(|e| e.name)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -98,23 +156,6 @@ impl<'a> ClientDb<'a> {
self.upsert_environment(&environment, source) self.upsert_environment(&environment, source)
} }
/// Find other environments with the same parent folder
fn list_duplicate_folder_environments(&self, environment: &Environment) -> Vec<Environment> {
if environment.parent_model != "folder" {
return Vec::new();
}
self.list_environments_dangerous(&environment.workspace_id)
.unwrap_or_default()
.into_iter()
.filter(|e| {
e.id != environment.id
&& e.parent_model == "folder"
&& e.parent_id == environment.parent_id
})
.collect()
}
pub fn upsert_environment( pub fn upsert_environment(
&self, &self,
environment: &Environment, environment: &Environment,
@@ -154,43 +195,4 @@ impl<'a> ClientDb<'a> {
source, source,
) )
} }
pub fn resolve_environments(
&self,
workspace_id: &str,
folder_id: Option<&str>,
active_environment_id: Option<&str>,
) -> Result<Vec<Environment>> {
let mut environments = Vec::new();
if let Some(folder_id) = folder_id {
let folder = self.get_folder(folder_id)?;
// Add current folder's environment
if let Some(e) = self.get_environment_by_folder_id(folder_id)? {
environments.push(e);
};
// Recurse up
let ancestors = self.resolve_environments(
workspace_id,
folder.folder_id.as_deref(),
active_environment_id,
)?;
environments.extend(ancestors);
} else {
// Add active and base environments
if let Some(id) = active_environment_id {
if let Ok(e) = self.get_environment(&id) {
// Add active sub environment
environments.push(e);
};
};
// Add the base environment
environments.push(self.get_base_environment(workspace_id)?);
}
Ok(environments)
}
} }
+96 -94
View File
@@ -1,5 +1,5 @@
use super::{conflict_free_name, merge_headers}; use super::{conflict_free_name, merge_headers};
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::connection_or_tx::ConnectionOrTx; use crate::connection_or_tx::ConnectionOrTx;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
@@ -20,99 +20,6 @@ impl<'a> ClientDb<'a> {
self.find_many(FolderIden::WorkspaceId, workspace_id, None) self.find_many(FolderIden::WorkspaceId, workspace_id, None)
} }
pub fn delete_folder(&self, folder: &Folder, source: &UpdateSource) -> Result<Folder> {
match self.conn() {
ConnectionOrTx::Connection(_) => {}
ConnectionOrTx::Transaction(_) => {}
}
let fid = &folder.id;
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.delete_http_request(&m, source)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.delete_grpc_request(&m, source)?;
}
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.delete_websocket_request(&m, source)?;
}
for e in self.find_many(EnvironmentIden::ParentId, fid, None)? {
self.delete_environment(&e, source)?;
}
// Recurse down into child folders
for folder in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
self.delete_folder(&folder, source)?;
}
self.delete(folder, source)
}
pub fn delete_folder_by_id(&self, id: &str, source: &UpdateSource) -> Result<Folder> {
let folder = self.get_folder(id)?;
self.delete_folder(&folder, source)
}
pub fn upsert_folder(&self, folder: &Folder, source: &UpdateSource) -> Result<Folder> {
self.upsert(folder, source)
}
pub fn duplicate_folder(&self, src_folder: &Folder, source: &UpdateSource) -> Result<Folder> {
let fid = &src_folder.id;
let mut folder = Folder {
id: "".into(),
sort_priority: src_folder.sort_priority + 0.001,
..src_folder.clone()
};
let sibling_names = self
.list_folders(&folder.workspace_id)?
.into_iter()
.filter(|f| f.folder_id == folder.folder_id)
.map(|f| f.name)
.collect::<Vec<_>>();
folder.name = conflict_free_name(&folder.name, &sibling_names);
let new_folder = self.upsert_folder(&folder, source)?;
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.upsert_http_request(
&HttpRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.upsert_websocket_request(
&WebsocketRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.upsert_grpc_request(
&GrpcRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<Environment>(EnvironmentIden::ParentId, fid, None)? {
self.upsert_environment(
&Environment { id: "".into(), parent_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
// Recurse down
self.duplicate_folder(&Folder { folder_id: Some(new_folder.id.clone()), ..m }, source)?;
}
Ok(new_folder)
}
pub fn resolve_auth_for_folder( pub fn resolve_auth_for_folder(
&self, &self,
folder: &Folder, folder: &Folder,
@@ -219,3 +126,98 @@ impl<'a> ClientDb<'a> {
}) })
} }
} }
impl<'a> WriteDb<'a> {
pub fn delete_folder(&self, folder: &Folder, source: &UpdateSource) -> Result<Folder> {
match self.conn() {
ConnectionOrTx::Connection(_) => {}
ConnectionOrTx::Transaction(_) => {}
}
let fid = &folder.id;
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.delete_http_request(&m, source)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.delete_grpc_request(&m, source)?;
}
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.delete_websocket_request(&m, source)?;
}
for e in self.find_many(EnvironmentIden::ParentId, fid, None)? {
self.delete_environment(&e, source)?;
}
// Recurse down into child folders
for folder in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
self.delete_folder(&folder, source)?;
}
self.delete(folder, source)
}
pub fn delete_folder_by_id(&self, id: &str, source: &UpdateSource) -> Result<Folder> {
let folder = self.get_folder(id)?;
self.delete_folder(&folder, source)
}
pub fn upsert_folder(&self, folder: &Folder, source: &UpdateSource) -> Result<Folder> {
self.upsert(folder, source)
}
pub fn duplicate_folder(&self, src_folder: &Folder, source: &UpdateSource) -> Result<Folder> {
let fid = &src_folder.id;
let mut folder = Folder {
id: "".into(),
sort_priority: src_folder.sort_priority + 0.001,
..src_folder.clone()
};
let sibling_names = self
.list_folders(&folder.workspace_id)?
.into_iter()
.filter(|f| f.folder_id == folder.folder_id)
.map(|f| f.name)
.collect::<Vec<_>>();
folder.name = conflict_free_name(&folder.name, &sibling_names);
let new_folder = self.upsert_folder(&folder, source)?;
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.upsert_http_request(
&HttpRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.upsert_websocket_request(
&WebsocketRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.upsert_grpc_request(
&GrpcRequest { id: "".into(), folder_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<Environment>(EnvironmentIden::ParentId, fid, None)? {
self.upsert_environment(
&Environment { id: "".into(), parent_id: Some(new_folder.id.clone()), ..m },
source,
)?;
}
for m in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
// Recurse down
self.duplicate_folder(&Folder { folder_id: Some(new_folder.id.clone()), ..m }, source)?;
}
Ok(new_folder)
}
}
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden}; use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
pub fn get_graphql_introspection(&self, request_id: &str) -> Option<GraphQlIntrospection> { pub fn get_graphql_introspection(&self, request_id: &str) -> Option<GraphQlIntrospection> {
self.find_optional(GraphQlIntrospectionIden::RequestId, request_id) self.find_optional(GraphQlIntrospectionIden::RequestId, request_id)
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_graphql_introspection( pub fn upsert_graphql_introspection(
&self, &self,
workspace_id: &str, workspace_id: &str,
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState}; use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState};
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
@@ -13,6 +13,20 @@ impl<'a> ClientDb<'a> {
self.find_one(GrpcConnectionIden::Id, id) self.find_one(GrpcConnectionIden::Id, id)
} }
pub fn list_grpc_connections_for_request(
&self,
request_id: &str,
limit: Option<u64>,
) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::RequestId, request_id, limit)
}
pub fn list_grpc_connections(&self, workspace_id: &str) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::WorkspaceId, workspace_id, None)
}
}
impl<'a> WriteDb<'a> {
pub fn delete_all_grpc_connections_for_request( pub fn delete_all_grpc_connections_for_request(
&self, &self,
request_id: &str, request_id: &str,
@@ -53,18 +67,6 @@ impl<'a> ClientDb<'a> {
self.delete_grpc_connection(&grpc_connection, source) self.delete_grpc_connection(&grpc_connection, source)
} }
pub fn list_grpc_connections_for_request(
&self,
request_id: &str,
limit: Option<u64>,
) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::RequestId, request_id, limit)
}
pub fn list_grpc_connections(&self, workspace_id: &str) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::WorkspaceId, workspace_id, None)
}
pub fn cancel_pending_grpc_connections(&self) -> Result<()> { pub fn cancel_pending_grpc_connections(&self) -> Result<()> {
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?; let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
let (sql, params) = Query::update() let (sql, params) = Query::update()
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{GrpcEvent, GrpcEventIden}; use crate::models::{GrpcEvent, GrpcEventIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
pub fn list_grpc_events(&self, connection_id: &str) -> Result<Vec<GrpcEvent>> { pub fn list_grpc_events(&self, connection_id: &str) -> Result<Vec<GrpcEvent>> {
self.find_many(GrpcEventIden::ConnectionId, connection_id, None) self.find_many(GrpcEventIden::ConnectionId, connection_id, None)
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_grpc_event( pub fn upsert_grpc_event(
&self, &self,
grpc_event: &GrpcEvent, grpc_event: &GrpcEvent,
+47 -45
View File
@@ -1,5 +1,5 @@
use super::{conflict_free_name, merge_headers}; use super::{conflict_free_name, merge_headers};
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
AnyModel, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequestHeader, AnyModel, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequestHeader,
@@ -32,50 +32,6 @@ impl<'a> ClientDb<'a> {
Ok(children) Ok(children)
} }
pub fn delete_grpc_request(
&self,
m: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
self.delete_all_grpc_connections_for_request(m.id.as_str(), source)?;
self.delete(m, source)
}
pub fn delete_grpc_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<GrpcRequest> {
let request = self.get_grpc_request(id)?;
self.delete_grpc_request(&request, source)
}
pub fn duplicate_grpc_request(
&self,
grpc_request: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
let mut request = grpc_request.clone();
request.id = "".to_string();
request.sort_priority = request.sort_priority + 0.001;
let sibling_names = self
.list_grpc_requests(&request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
request.name = conflict_free_name(&request.name, &sibling_names);
self.upsert(&request, source)
}
pub fn upsert_grpc_request(
&self,
grpc_request: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
self.upsert(grpc_request, source)
}
pub fn resolve_auth_for_grpc_request( pub fn resolve_auth_for_grpc_request(
&self, &self,
grpc_request: &GrpcRequest, grpc_request: &GrpcRequest,
@@ -146,3 +102,49 @@ impl<'a> ClientDb<'a> {
}) })
} }
} }
impl<'a> WriteDb<'a> {
pub fn delete_grpc_request(
&self,
m: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
self.delete_all_grpc_connections_for_request(m.id.as_str(), source)?;
self.delete(m, source)
}
pub fn delete_grpc_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<GrpcRequest> {
let request = self.get_grpc_request(id)?;
self.delete_grpc_request(&request, source)
}
pub fn duplicate_grpc_request(
&self,
grpc_request: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
let mut request = grpc_request.clone();
request.id = "".to_string();
request.sort_priority = request.sort_priority + 0.001;
let sibling_names = self
.list_grpc_requests(&request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
request.name = conflict_free_name(&request.name, &sibling_names);
self.upsert(&request, source)
}
pub fn upsert_grpc_request(
&self,
grpc_request: &GrpcRequest,
source: &UpdateSource,
) -> Result<GrpcRequest> {
self.upsert(grpc_request, source)
}
}
+83 -70
View File
@@ -1,5 +1,5 @@
use super::{conflict_free_name, merge_headers}; use super::{conflict_free_name, merge_headers};
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
AnyModel, Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden, AnyModel, Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden,
@@ -18,50 +18,6 @@ impl<'a> ClientDb<'a> {
self.find_many(HttpRequestIden::WorkspaceId, workspace_id, None) self.find_many(HttpRequestIden::WorkspaceId, workspace_id, None)
} }
pub fn delete_http_request(
&self,
m: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
self.delete_all_http_responses_for_request(m.id.as_str(), source)?;
self.delete(m, source)
}
pub fn delete_http_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<HttpRequest> {
let http_request = self.get_http_request(id)?;
self.delete_http_request(&http_request, source)
}
pub fn duplicate_http_request(
&self,
http_request: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
let mut http_request = http_request.clone();
http_request.id = "".to_string();
http_request.sort_priority = http_request.sort_priority + 0.001;
let sibling_names = self
.list_http_requests(&http_request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == http_request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
http_request.name = conflict_free_name(&http_request.name, &sibling_names);
self.upsert(&http_request, source)
}
pub fn upsert_http_request(
&self,
http_request: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
self.upsert(http_request, source)
}
pub fn resolve_auth_for_http_request( pub fn resolve_auth_for_http_request(
&self, &self,
http_request: &HttpRequest, http_request: &HttpRequest,
@@ -179,6 +135,52 @@ impl<'a> ClientDb<'a> {
} }
} }
impl<'a> WriteDb<'a> {
pub fn delete_http_request(
&self,
m: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
self.delete_all_http_responses_for_request(m.id.as_str(), source)?;
self.delete(m, source)
}
pub fn delete_http_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<HttpRequest> {
let http_request = self.get_http_request(id)?;
self.delete_http_request(&http_request, source)
}
pub fn duplicate_http_request(
&self,
http_request: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
let mut http_request = http_request.clone();
http_request.id = "".to_string();
http_request.sort_priority = http_request.sort_priority + 0.001;
let sibling_names = self
.list_http_requests(&http_request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == http_request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
http_request.name = conflict_free_name(&http_request.name, &sibling_names);
self.upsert(&http_request, source)
}
pub fn upsert_http_request(
&self,
http_request: &HttpRequest,
source: &UpdateSource,
) -> Result<HttpRequest> {
self.upsert(http_request, source)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::init_in_memory; use crate::init_in_memory;
@@ -225,43 +227,45 @@ mod tests {
#[test] #[test]
fn http_version_resolves_through_the_inheritance_chain() { fn http_version_resolves_through_the_inheritance_chain() {
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); let source = &UpdateSource::Background;
let workspace = db let (folder, request) = query_manager
.upsert_workspace( .with_tx(|db| {
let workspace = db.upsert_workspace(
&Workspace { &Workspace {
name: "Test".to_string(), name: "Test".to_string(),
setting_http_version: HttpVersion::Http2, setting_http_version: HttpVersion::Http2,
..Default::default() ..Default::default()
}, },
&UpdateSource::Background, source,
) )?;
.expect("Failed to upsert workspace"); let folder = db.upsert_folder(
let folder = db
.upsert_folder(
&Folder { workspace_id: workspace.id.clone(), ..Default::default() }, &Folder { workspace_id: workspace.id.clone(), ..Default::default() },
&UpdateSource::Background, source,
) )?;
.expect("Failed to upsert folder"); let request = db.upsert_http_request(
let request = db
.upsert_http_request(
&HttpRequest { &HttpRequest {
workspace_id: workspace.id.clone(), workspace_id: workspace.id.clone(),
folder_id: Some(folder.id.clone()), folder_id: Some(folder.id.clone()),
..Default::default() ..Default::default()
}, },
&UpdateSource::Background, source,
) )?;
.expect("Failed to upsert request"); Ok::<_, crate::error::Error>((folder, request))
})
.expect("Failed to seed");
// No overrides, so the workspace base value applies // No overrides, so the workspace base value applies
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve"); let resolved = query_manager
.connect()
.resolve_settings_for_http_request(&request)
.expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Http2); assert_eq!(resolved.http_version.value, HttpVersion::Http2);
assert_eq!(resolved.http_version.source_model, "workspace"); assert_eq!(resolved.http_version.source_model, "workspace");
// A folder override beats the workspace base // A folder override beats the workspace base
query_manager
.with_tx(|db| {
db.upsert_folder( db.upsert_folder(
&Folder { &Folder {
setting_http_version: InheritedHttpVersionSetting { setting_http_version: InheritedHttpVersionSetting {
@@ -270,16 +274,21 @@ mod tests {
}, },
..folder ..folder
}, },
&UpdateSource::Background, source,
) )
})
.expect("Failed to update folder"); .expect("Failed to update folder");
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve"); let resolved = query_manager
.connect()
.resolve_settings_for_http_request(&request)
.expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Http1); assert_eq!(resolved.http_version.value, HttpVersion::Http1);
assert_eq!(resolved.http_version.source_model, "folder"); assert_eq!(resolved.http_version.source_model, "folder");
// A request override beats them both // A request override beats them both
let request = db let request = query_manager
.upsert_http_request( .with_tx(|db| {
db.upsert_http_request(
&HttpRequest { &HttpRequest {
setting_http_version: InheritedHttpVersionSetting { setting_http_version: InheritedHttpVersionSetting {
enabled: true, enabled: true,
@@ -287,10 +296,14 @@ mod tests {
}, },
..request ..request
}, },
&UpdateSource::Background, source,
) )
})
.expect("Failed to update request"); .expect("Failed to update request");
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve"); let resolved = query_manager
.connect()
.resolve_settings_for_http_request(&request)
.expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Auto); assert_eq!(resolved.http_version.value, HttpVersion::Auto);
assert_eq!(resolved.http_version.source_model, "http_request"); assert_eq!(resolved.http_version.source_model, "http_request");
} }
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{HttpResponseEvent, HttpResponseEventIden}; use crate::models::{HttpResponseEvent, HttpResponseEventIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -7,7 +7,9 @@ impl<'a> ClientDb<'a> {
pub fn list_http_response_events(&self, response_id: &str) -> Result<Vec<HttpResponseEvent>> { pub fn list_http_response_events(&self, response_id: &str) -> Result<Vec<HttpResponseEvent>> {
self.find_many(HttpResponseEventIden::ResponseId, response_id, None) self.find_many(HttpResponseEventIden::ResponseId, response_id, None)
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_http_response_event( pub fn upsert_http_response_event(
&self, &self,
http_response_event: &HttpResponseEvent, http_response_event: &HttpResponseEvent,
@@ -1,5 +1,5 @@
use crate::blob_manager::BlobManager; use crate::blob_manager::BlobManager;
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState}; use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState};
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
@@ -31,20 +31,6 @@ impl<'a> ClientDb<'a> {
self.find_many(HttpResponseIden::WorkspaceId, workspace_id, limit) self.find_many(HttpResponseIden::WorkspaceId, workspace_id, limit)
} }
/// Returns the number of responses deleted.
pub fn delete_all_http_responses_for_request(
&self,
request_id: &str,
source: &UpdateSource,
) -> Result<usize> {
let responses = self.list_http_responses_for_request(request_id, None)?;
let count = responses.len();
for m in responses {
self.delete(&m, source)?;
}
Ok(count)
}
/// Delete blob-stored response bodies whose owning HTTP response row no /// Delete blob-stored response bodies whose owning HTTP response row no
/// longer exists. Blob ids are keyed by the response that owns them — /// longer exists. Blob ids are keyed by the response that owns them —
/// "{response_id}" for a response body, "{response_id}.request" for the /// "{response_id}" for a response body, "{response_id}.request" for the
@@ -55,19 +41,24 @@ impl<'a> ClientDb<'a> {
/// ///
/// Returns the number of orphaned bodies deleted. /// Returns the number of orphaned bodies deleted.
pub fn delete_orphaned_response_body_blobs(&self, blobs: &BlobManager) -> Result<usize> { pub fn delete_orphaned_response_body_blobs(&self, blobs: &BlobManager) -> Result<usize> {
let mut deleted = 0; let orphaned = blobs
.connect()
let blob_ctx = blobs.connect(); .list_body_ids()?
for body_id in blob_ctx.list_body_ids()? { .into_iter()
.filter(|body_id| {
let response_id = body_id.split('.').next().unwrap_or_default(); let response_id = body_id.split('.').next().unwrap_or_default();
if self.find_optional::<HttpResponse>(HttpResponseIden::Id, response_id).is_some() { self.find_optional::<HttpResponse>(HttpResponseIden::Id, response_id).is_none()
continue; })
} .collect::<Vec<_>>();
blob_ctx.delete_chunks(&body_id)?;
deleted += 1;
}
Ok(deleted) blobs.with_tx(|b| {
for body_id in &orphaned {
b.delete_chunks(body_id)?;
}
Ok::<_, crate::error::Error>(())
})?;
Ok(orphaned.len())
} }
/// Delete response body data (blob chunks and body files) whose owning HTTP /// Delete response body data (blob chunks and body files) whose owning HTTP
@@ -107,6 +98,22 @@ impl<'a> ClientDb<'a> {
Ok(deleted) Ok(deleted)
} }
}
impl<'a> WriteDb<'a> {
/// Returns the number of responses deleted.
pub fn delete_all_http_responses_for_request(
&self,
request_id: &str,
source: &UpdateSource,
) -> Result<usize> {
let responses = self.list_http_responses_for_request(request_id, None)?;
let count = responses.len();
for m in responses {
self.delete(&m, source)?;
}
Ok(count)
}
/// Returns the number of responses deleted. /// Returns the number of responses deleted.
pub fn delete_all_http_responses_for_workspace( pub fn delete_all_http_responses_for_workspace(
@@ -137,9 +144,8 @@ impl<'a> ClientDb<'a> {
} }
// Delete request body blobs (pattern: {response_id}.request) // Delete request body blobs (pattern: {response_id}.request)
let blob_ctx = blob_manager.connect();
let body_id = format!("{}.request", http_response.id); let body_id = format!("{}.request", http_response.id);
if let Err(e) = blob_ctx.delete_chunks(&body_id) { if let Err(e) = blob_manager.with_tx(|b| b.delete_chunks(&body_id)) {
error!("Failed to delete request body blobs: {}", e); error!("Failed to delete request body blobs: {}", e);
} }
@@ -186,26 +192,28 @@ impl<'a> ClientDb<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::blob_manager::{BlobManager, BodyChunk}; use crate::blob_manager::{BlobManager, BodyChunk};
use crate::client_db::ClientDb; use crate::error::Error;
use crate::init_in_memory; use crate::init_in_memory;
use crate::models::{HttpRequest, HttpResponse, Workspace}; use crate::models::{HttpRequest, HttpResponse, Workspace};
use crate::query_manager::QueryManager;
use crate::util::UpdateSource; use crate::util::UpdateSource;
/// A workspace, a request, and one response that still exists. /// A workspace, a request, and one response that still exists.
fn seed_live_response(db: &ClientDb, blob_manager: &BlobManager) -> HttpResponse { fn seed_live_response(
query_manager: &QueryManager,
blob_manager: &BlobManager,
) -> HttpResponse {
let source = &UpdateSource::Background; let source = &UpdateSource::Background;
let workspace = db query_manager
.upsert_workspace( .with_tx(|db| {
let workspace = db.upsert_workspace(
&Workspace { name: "GC Test".to_string(), ..Default::default() }, &Workspace { name: "GC Test".to_string(), ..Default::default() },
source, source,
) )?;
.expect("Failed to upsert workspace"); let request = db.upsert_http_request(
let request = db
.upsert_http_request(
&HttpRequest { workspace_id: workspace.id.clone(), ..Default::default() }, &HttpRequest { workspace_id: workspace.id.clone(), ..Default::default() },
source, source,
) )?;
.expect("Failed to upsert request");
db.upsert_http_response( db.upsert_http_response(
&HttpResponse { &HttpResponse {
request_id: request.id.clone(), request_id: request.id.clone(),
@@ -215,7 +223,19 @@ mod tests {
source, source,
blob_manager, blob_manager,
) )
.expect("Failed to upsert response") })
.expect("Failed to seed response")
}
fn insert_bodies(blob_manager: &BlobManager, body_ids: &[&str]) {
blob_manager
.with_tx(|b| {
for id in body_ids {
b.insert_chunk(&BodyChunk::new(*id, 0, b"data".to_vec()))?;
}
Ok::<_, Error>(())
})
.expect("Failed to insert chunks");
} }
/// What a browser host runs: no filesystem, so bodies exist only as blob /// What a browser host runs: no filesystem, so bodies exist only as blob
@@ -223,23 +243,21 @@ mod tests {
#[test] #[test]
fn deletes_orphaned_response_body_blobs() { fn deletes_orphaned_response_body_blobs() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect();
let live = seed_live_response(&db, &blob_manager); let live = seed_live_response(&query_manager, &blob_manager);
let live_request_body_id = format!("{}.request", live.id); let live_request_body_id = format!("{}.request", live.id);
{ insert_bodies(
// Scope the connection: the in-memory pool only has one, and the GC &blob_manager,
// needs to take it &[
let blob_ctx = blob_manager.connect(); &live.id,
blob_ctx.insert_chunk(&BodyChunk::new(&live.id, 0, b"live".to_vec())).unwrap(); &live_request_body_id,
blob_ctx "rs_gone",
.insert_chunk(&BodyChunk::new(&live_request_body_id, 0, b"live".to_vec())) "rs_gone.request",
.unwrap(); ],
blob_ctx.insert_chunk(&BodyChunk::new("rs_gone", 0, b"dead".to_vec())).unwrap(); );
blob_ctx.insert_chunk(&BodyChunk::new("rs_gone.request", 0, b"dead".to_vec())).unwrap();
}
let deleted = db let deleted = query_manager
.connect()
.delete_orphaned_response_body_blobs(&blob_manager) .delete_orphaned_response_body_blobs(&blob_manager)
.expect("Failed to GC response body blobs"); .expect("Failed to GC response body blobs");
assert_eq!(deleted, 2); assert_eq!(deleted, 2);
@@ -254,24 +272,18 @@ mod tests {
#[test] #[test]
fn deletes_orphaned_response_bodies() { fn deletes_orphaned_response_bodies() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect();
let live = seed_live_response(&db, &blob_manager); let live = seed_live_response(&query_manager, &blob_manager);
let live_body_id = format!("{}.request", live.id); let live_body_id = format!("{}.request", live.id);
{ insert_bodies(&blob_manager, &[&live_body_id, "rs_gone.request"]);
// Scope the connection: the in-memory pool only has one, and the GC
// needs to take it
let blob_ctx = blob_manager.connect();
blob_ctx.insert_chunk(&BodyChunk::new(&live_body_id, 0, b"live".to_vec())).unwrap();
blob_ctx.insert_chunk(&BodyChunk::new("rs_gone.request", 0, b"dead".to_vec())).unwrap();
}
let dir = std::env::temp_dir().join(format!("yaak-blob-gc-test-{}", live.id)); let dir = std::env::temp_dir().join(format!("yaak-blob-gc-test-{}", live.id));
std::fs::create_dir_all(&dir).unwrap(); std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(&live.id), b"live").unwrap(); std::fs::write(dir.join(&live.id), b"live").unwrap();
std::fs::write(dir.join("rs_gone"), b"dead").unwrap(); std::fs::write(dir.join("rs_gone"), b"dead").unwrap();
let deleted = db let deleted = query_manager
.connect()
.delete_orphaned_response_bodies(&blob_manager, &dir) .delete_orphaned_response_bodies(&blob_manager, &dir)
.expect("Failed to GC response bodies"); .expect("Failed to GC response bodies");
assert_eq!(deleted, 2); assert_eq!(deleted, 2);
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ImportSourceResource, ImportSourceResourceIden}; use crate::models::{ImportSourceResource, ImportSourceResourceIden};
use sea_query::ExprTrait; use sea_query::ExprTrait;
@@ -20,7 +20,9 @@ impl<'a> ClientDb<'a> {
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?; let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.filter_map(|v| v.ok()).collect()) Ok(items.filter_map(|v| v.ok()).collect())
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_import_source_resource( pub fn upsert_import_source_resource(
&self, &self,
resource: &ImportSourceResource, resource: &ImportSourceResource,
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ImportSource, ImportSourceIden}; use crate::models::{ImportSource, ImportSourceIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -25,7 +25,9 @@ impl<'a> ClientDb<'a> {
let sources = self.list_import_sources(workspace_id)?; let sources = self.list_import_sources(workspace_id)?;
Ok(sources.into_iter().find(|s| s.importer == importer && s.origin == origin)) Ok(sources.into_iter().find(|s| s.importer == importer && s.origin == origin))
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_import_source( pub fn upsert_import_source(
&self, &self,
import_source: &ImportSource, import_source: &ImportSource,
+3 -1
View File
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo}; use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -89,7 +89,9 @@ impl<'a> ClientDb<'a> {
.build_rusqlite(SqliteQueryBuilder); .build_rusqlite(SqliteQueryBuilder);
self.conn().resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok() self.conn().resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok()
} }
}
impl<'a> WriteDb<'a> {
pub fn set_key_value_dte( pub fn set_key_value_dte(
&self, &self,
namespace: &str, namespace: &str,
+58 -17
View File
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::util::ModelPayload; use crate::util::ModelPayload;
use rusqlite::params; use rusqlite::params;
@@ -69,7 +69,9 @@ impl<'a> ClientDb<'a> {
Ok(items.collect::<std::result::Result<Vec<_>, rusqlite::Error>>()?) Ok(items.collect::<std::result::Result<Vec<_>, rusqlite::Error>>()?)
} }
}
impl<'a> WriteDb<'a> {
pub fn prune_model_changes_older_than_days(&self, days: i64) -> Result<usize> { pub fn prune_model_changes_older_than_days(&self, days: i64) -> Result<usize> {
let offset = format!("-{days} days"); let offset = format!("-{days} days");
Ok(self.conn().resolve().execute( Ok(self.conn().resolve().execute(
@@ -101,13 +103,24 @@ mod tests {
use crate::util::{ModelChangeEvent, UpdateSource}; use crate::util::{ModelChangeEvent, UpdateSource};
use serde_json::json; use serde_json::json;
/// Startup bootstraps rows of its own; these tests count only their own.
fn clear_changes(query_manager: &crate::query_manager::QueryManager) {
query_manager
.with_tx(|db| {
db.conn().resolve().execute("DELETE FROM model_changes", [])?;
Ok::<_, crate::error::Error>(())
})
.expect("Failed to clear model changes");
}
#[test] #[test]
fn records_model_changes_for_upsert_and_delete() { fn records_model_changes_for_upsert_and_delete() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); clear_changes(&query_manager);
let workspace = db let workspace = query_manager
.upsert_workspace( .with_tx(|db| {
db.upsert_workspace(
&Workspace { &Workspace {
name: "Changes Test".to_string(), name: "Changes Test".to_string(),
setting_follow_redirects: true, setting_follow_redirects: true,
@@ -116,8 +129,10 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to upsert workspace"); .expect("Failed to upsert workspace");
let db = query_manager.connect();
let created_changes = db.list_model_changes_after(0, 10).expect("Failed to list changes"); let created_changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
assert_eq!(created_changes.len(), 1); assert_eq!(created_changes.len(), 1);
assert_eq!(created_changes[0].payload.model.id(), workspace.id); assert_eq!(created_changes[0].payload.model.id(), workspace.id);
@@ -128,9 +143,14 @@ mod tests {
)); ));
assert!(matches!(created_changes[0].payload.update_source, UpdateSource::Sync)); assert!(matches!(created_changes[0].payload.update_source, UpdateSource::Sync));
drop(db);
query_manager
.with_tx(|db| {
db.delete_workspace_by_id(&workspace.id, &UpdateSource::Sync, &blob_manager) db.delete_workspace_by_id(&workspace.id, &UpdateSource::Sync, &blob_manager)
})
.expect("Failed to delete workspace"); .expect("Failed to delete workspace");
let db = query_manager.connect();
let all_changes = db.list_model_changes_after(0, 10).expect("Failed to list changes"); let all_changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
assert_eq!(all_changes.len(), 2); assert_eq!(all_changes.len(), 2);
assert!(matches!(all_changes[1].payload.change, ModelChangeEvent::Delete)); assert!(matches!(all_changes[1].payload.change, ModelChangeEvent::Delete));
@@ -146,8 +166,10 @@ mod tests {
#[test] #[test]
fn prunes_old_model_changes() { fn prunes_old_model_changes() {
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); clear_changes(&query_manager);
query_manager
.with_tx(|db| {
db.upsert_workspace( db.upsert_workspace(
&Workspace { &Workspace {
name: "Prune Test".to_string(), name: "Prune Test".to_string(),
@@ -157,8 +179,10 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to upsert workspace"); .expect("Failed to upsert workspace");
let db = query_manager.connect();
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes"); let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
assert_eq!(changes.len(), 1); assert_eq!(changes.len(), 1);
@@ -170,19 +194,28 @@ mod tests {
) )
.expect("Failed to age model change row"); .expect("Failed to age model change row");
let pruned = drop(db);
db.prune_model_changes_older_than_days(30).expect("Failed to prune model changes"); let pruned = query_manager
.with_tx(|db| db.prune_model_changes_older_than_days(30))
.expect("Failed to prune model changes");
assert_eq!(pruned, 1); assert_eq!(pruned, 1);
assert!(db.list_model_changes_after(0, 10).expect("Failed to list changes").is_empty()); assert!(
query_manager
.connect()
.list_model_changes_after(0, 10)
.expect("Failed to list changes")
.is_empty()
);
} }
#[test] #[test]
fn list_model_changes_since_uses_timestamp_with_id_tiebreaker() { fn list_model_changes_since_uses_timestamp_with_id_tiebreaker() {
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); clear_changes(&query_manager);
let workspace = db query_manager
.upsert_workspace( .with_tx(|db| {
let workspace = db.upsert_workspace(
&Workspace { &Workspace {
name: "Cursor Test".to_string(), name: "Cursor Test".to_string(),
setting_follow_redirects: true, setting_follow_redirects: true,
@@ -190,11 +223,12 @@ mod tests {
..Default::default() ..Default::default()
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )?;
.expect("Failed to upsert workspace");
db.delete_workspace_by_id(&workspace.id, &UpdateSource::Sync, &blob_manager) db.delete_workspace_by_id(&workspace.id, &UpdateSource::Sync, &blob_manager)
.expect("Failed to delete workspace"); })
.expect("Failed to seed changes");
let db = query_manager.connect();
let all = db.list_model_changes_after(0, 10).expect("Failed to list changes"); let all = db.list_model_changes_after(0, 10).expect("Failed to list changes");
assert_eq!(all.len(), 2); assert_eq!(all.len(), 2);
@@ -213,8 +247,10 @@ mod tests {
#[test] #[test]
fn prunes_old_model_changes_by_hours() { fn prunes_old_model_changes_by_hours() {
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect(); clear_changes(&query_manager);
query_manager
.with_tx(|db| {
db.upsert_workspace( db.upsert_workspace(
&Workspace { &Workspace {
name: "Prune Hour Test".to_string(), name: "Prune Hour Test".to_string(),
@@ -224,8 +260,10 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to upsert workspace"); .expect("Failed to upsert workspace");
let db = query_manager.connect();
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes"); let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
assert_eq!(changes.len(), 1); assert_eq!(changes.len(), 1);
@@ -237,14 +275,17 @@ mod tests {
) )
.expect("Failed to age model change row"); .expect("Failed to age model change row");
let pruned = drop(db);
db.prune_model_changes_older_than_hours(1).expect("Failed to prune model changes"); let pruned = query_manager
.with_tx(|db| db.prune_model_changes_older_than_hours(1))
.expect("Failed to prune model changes");
assert_eq!(pruned, 1); assert_eq!(pruned, 1);
} }
#[test] #[test]
fn list_model_changes_deserializes_http_response_event_payload() { fn list_model_changes_deserializes_http_response_event_payload() {
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
clear_changes(&query_manager);
let db = query_manager.connect(); let db = query_manager.connect();
let payload = json!({ let payload = json!({
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{PluginKeyValue, PluginKeyValueIden}; use crate::models::{PluginKeyValue, PluginKeyValueIden};
use sea_query::ExprTrait; use sea_query::ExprTrait;
@@ -22,7 +22,9 @@ impl<'a> ClientDb<'a> {
.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()) .query_row(sql.as_str(), &*params.as_params(), |row| row.try_into())
.ok() .ok()
} }
}
impl<'a> WriteDb<'a> {
pub fn set_plugin_key_value( pub fn set_plugin_key_value(
&self, &self,
plugin_name: &str, plugin_name: &str,
+3 -1
View File
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{Plugin, PluginIden}; use crate::models::{Plugin, PluginIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -15,7 +15,9 @@ impl<'a> ClientDb<'a> {
pub fn list_plugins(&self) -> Result<Vec<Plugin>> { pub fn list_plugins(&self) -> Result<Vec<Plugin>> {
self.find_all() self.find_all()
} }
}
impl<'a> WriteDb<'a> {
pub fn delete_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> { pub fn delete_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
self.delete(plugin, source) self.delete(plugin, source)
} }
+22 -13
View File
@@ -1,21 +1,36 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{EditorKeymap, Settings, SettingsIden}; use crate::models::{EditorKeymap, Settings, SettingsIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
impl<'a> ClientDb<'a> { impl<'a> ClientDb<'a> {
/// The settings row, or the defaults if it has not been written yet.
/// [`WriteDb::ensure_settings`] persists it at startup.
pub fn get_settings(&self) -> Settings { pub fn get_settings(&self) -> Settings {
let id = "default".to_string(); self.find_optional::<Settings>(SettingsIden::Id, "default").unwrap_or_else(default_settings)
}
}
if let Some(s) = self.find_optional::<Settings>(SettingsIden::Id, &id) { impl<'a> WriteDb<'a> {
return s; /// Create the settings row if it does not exist.
}; pub fn ensure_settings(&self) -> Result<Settings> {
if let Some(s) = self.find_optional::<Settings>(SettingsIden::Id, "default") {
return Ok(s);
}
self.upsert(&default_settings(), &UpdateSource::Background)
}
let settings = Settings { pub fn upsert_settings(&self, settings: &Settings, source: &UpdateSource) -> Result<Settings> {
self.upsert(settings, source)
}
}
fn default_settings() -> Settings {
Settings {
model: "settings".to_string(), model: "settings".to_string(),
id, id: "default".to_string(),
created_at: Default::default(), created_at: Default::default(),
updated_at: Default::default(), updated_at: Default::default(),
@@ -42,11 +57,5 @@ impl<'a> ClientDb<'a> {
auto_download_updates: true, auto_download_updates: true,
check_notifications: true, check_notifications: true,
hotkeys: HashMap::new(), hotkeys: HashMap::new(),
};
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
}
pub fn upsert_settings(&self, settings: &Settings, source: &UpdateSource) -> Result<Settings> {
self.upsert(settings, source)
} }
} }
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{SyncState, SyncStateIden, UpsertModelInfo}; use crate::models::{SyncState, SyncStateIden, UpsertModelInfo};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -12,10 +12,6 @@ impl<'a> ClientDb<'a> {
self.find_one(SyncStateIden::Id, id) self.find_one(SyncStateIden::Id, id)
} }
pub fn upsert_sync_state(&self, sync_state: &SyncState) -> Result<SyncState> {
self.upsert(sync_state, &UpdateSource::Sync)
}
pub fn list_sync_states_for_workspace( pub fn list_sync_states_for_workspace(
&self, &self,
workspace_id: &str, workspace_id: &str,
@@ -34,6 +30,12 @@ impl<'a> ClientDb<'a> {
let items = stmt.query_map(&*params.as_params(), SyncState::from_row)?; let items = stmt.query_map(&*params.as_params(), SyncState::from_row)?;
Ok(items.map(|v| v.unwrap()).collect()) Ok(items.map(|v| v.unwrap()).collect())
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_sync_state(&self, sync_state: &SyncState) -> Result<SyncState> {
self.upsert(sync_state, &UpdateSource::Sync)
}
pub fn delete_sync_state(&self, sync_state: &SyncState) -> Result<SyncState> { pub fn delete_sync_state(&self, sync_state: &SyncState) -> Result<SyncState> {
self.delete(sync_state, &UpdateSource::Sync) self.delete(sync_state, &UpdateSource::Sync)
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConnectionState}; use crate::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConnectionState};
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
@@ -13,6 +13,22 @@ impl<'a> ClientDb<'a> {
self.find_one(WebsocketConnectionIden::Id, id) self.find_one(WebsocketConnectionIden::Id, id)
} }
pub fn list_websocket_connections(
&self,
workspace_id: &str,
) -> Result<Vec<WebsocketConnection>> {
self.find_many(WebsocketConnectionIden::WorkspaceId, workspace_id, None)
}
pub fn list_websocket_connections_for_request(
&self,
request_id: &str,
) -> Result<Vec<WebsocketConnection>> {
self.find_many(WebsocketConnectionIden::RequestId, request_id, None)
}
}
impl<'a> WriteDb<'a> {
pub fn delete_all_websocket_connections_for_request( pub fn delete_all_websocket_connections_for_request(
&self, &self,
request_id: &str, request_id: &str,
@@ -37,20 +53,6 @@ impl<'a> ClientDb<'a> {
Ok(()) Ok(())
} }
pub fn list_websocket_connections(
&self,
workspace_id: &str,
) -> Result<Vec<WebsocketConnection>> {
self.find_many(WebsocketConnectionIden::WorkspaceId, workspace_id, None)
}
pub fn list_websocket_connections_for_request(
&self,
request_id: &str,
) -> Result<Vec<WebsocketConnection>> {
self.find_many(WebsocketConnectionIden::RequestId, request_id, None)
}
pub fn delete_websocket_connection( pub fn delete_websocket_connection(
&self, &self,
websocket_connection: &WebsocketConnection, websocket_connection: &WebsocketConnection,
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{WebsocketEvent, WebsocketEventIden}; use crate::models::{WebsocketEvent, WebsocketEventIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
pub fn list_websocket_events(&self, connection_id: &str) -> Result<Vec<WebsocketEvent>> { pub fn list_websocket_events(&self, connection_id: &str) -> Result<Vec<WebsocketEvent>> {
self.find_many(WebsocketEventIden::ConnectionId, connection_id, None) self.find_many(WebsocketEventIden::ConnectionId, connection_id, None)
} }
}
impl<'a> WriteDb<'a> {
pub fn upsert_websocket_event( pub fn upsert_websocket_event(
&self, &self,
websocket_event: &WebsocketEvent, websocket_event: &WebsocketEvent,
@@ -1,5 +1,5 @@
use super::{conflict_free_name, merge_headers}; use super::{conflict_free_name, merge_headers};
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
AnyModel, Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, ResolvedSetting, AnyModel, Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, ResolvedSetting,
@@ -34,50 +34,6 @@ impl<'a> ClientDb<'a> {
Ok(children) Ok(children)
} }
pub fn delete_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
self.delete_all_websocket_connections_for_request(websocket_request.id.as_str(), source)?;
self.delete(websocket_request, source)
}
pub fn delete_websocket_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
let request = self.get_websocket_request(id)?;
self.delete_websocket_request(&request, source)
}
pub fn duplicate_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
let mut websocket_request = websocket_request.clone();
websocket_request.id = "".to_string();
websocket_request.sort_priority = websocket_request.sort_priority + 0.001;
let sibling_names = self
.list_websocket_requests(&websocket_request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == websocket_request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
websocket_request.name = conflict_free_name(&websocket_request.name, &sibling_names);
self.upsert(&websocket_request, source)
}
pub fn upsert_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
self.upsert(websocket_request, source)
}
pub fn resolve_auth_for_websocket_request( pub fn resolve_auth_for_websocket_request(
&self, &self,
websocket_request: &WebsocketRequest, websocket_request: &WebsocketRequest,
@@ -168,3 +124,49 @@ impl<'a> ClientDb<'a> {
}) })
} }
} }
impl<'a> WriteDb<'a> {
pub fn delete_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
self.delete_all_websocket_connections_for_request(websocket_request.id.as_str(), source)?;
self.delete(websocket_request, source)
}
pub fn delete_websocket_request_by_id(
&self,
id: &str,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
let request = self.get_websocket_request(id)?;
self.delete_websocket_request(&request, source)
}
pub fn duplicate_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
let mut websocket_request = websocket_request.clone();
websocket_request.id = "".to_string();
websocket_request.sort_priority = websocket_request.sort_priority + 0.001;
let sibling_names = self
.list_websocket_requests(&websocket_request.workspace_id)?
.into_iter()
.filter(|m| m.folder_id == websocket_request.folder_id)
.map(|m| m.name)
.collect::<Vec<_>>();
websocket_request.name = conflict_free_name(&websocket_request.name, &sibling_names);
self.upsert(&websocket_request, source)
}
pub fn upsert_websocket_request(
&self,
websocket_request: &WebsocketRequest,
source: &UpdateSource,
) -> Result<WebsocketRequest> {
self.upsert(websocket_request, source)
}
}
@@ -1,4 +1,4 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{WorkspaceMeta, WorkspaceMetaIden}; use crate::models::{WorkspaceMeta, WorkspaceMetaIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
@@ -10,18 +10,12 @@ impl<'a> ClientDb<'a> {
} }
pub fn list_workspace_metas(&self, workspace_id: &str) -> Result<Vec<WorkspaceMeta>> { pub fn list_workspace_metas(&self, workspace_id: &str) -> Result<Vec<WorkspaceMeta>> {
let mut workspace_metas = self.find_many(WorkspaceMetaIden::WorkspaceId, workspace_id, None)
self.find_many(WorkspaceMetaIden::WorkspaceId, workspace_id, None)?; }
if workspace_metas.is_empty() {
let wm = WorkspaceMeta { workspace_id: workspace_id.to_string(), ..Default::default() };
workspace_metas.push(self.upsert_workspace_meta(&wm, &UpdateSource::Background)?)
} }
Ok(workspace_metas) impl<'a> WriteDb<'a> {
} pub fn ensure_workspace_meta(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
pub fn get_or_create_workspace_meta(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
let workspace_meta = self.get_workspace_meta(workspace_id); let workspace_meta = self.get_workspace_meta(workspace_id);
if let Some(workspace_meta) = workspace_meta { if let Some(workspace_meta) = workspace_meta {
return Ok(workspace_meta); return Ok(workspace_meta);
+96 -114
View File
@@ -1,6 +1,6 @@
use super::merge_headers; use super::merge_headers;
use crate::blob_manager::BlobManager; use crate::blob_manager::BlobManager;
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden,
@@ -23,119 +23,7 @@ impl<'a> ClientDb<'a> {
} }
pub fn list_workspaces(&self) -> Result<Vec<Workspace>> { pub fn list_workspaces(&self) -> Result<Vec<Workspace>> {
let mut workspaces = self.find_all()?; self.find_all()
if workspaces.is_empty() {
workspaces.push(self.upsert_workspace(
&Workspace { name: "Yaak".to_string(), ..Default::default() },
&UpdateSource::Background,
)?)
}
Ok(workspaces)
}
/// Delete a workspace and everything in it.
///
/// Children are bulk-deleted with one statement per table and are NOT
/// individually recorded in model_changes or emitted as events — the single
/// workspace delete event implies the subtree (see [`ModelChangeEvent::Delete`]).
/// This keeps huge workspaces (thousands of requests) fast and avoids
/// flooding event consumers.
pub fn delete_workspace(
&self,
workspace: &Workspace,
source: &UpdateSource,
blobs: &BlobManager,
) -> Result<Workspace> {
let wid = workspace.id.as_str();
// Collect response cleanup targets before their rows disappear. The actual
// cleanup runs at the end: response bodies live on disk and in the blob DB,
// which don't participate in this transaction, so removing them must wait
// until every statement that could fail (and roll back the rows) is done.
let responses = self.find_many::<HttpResponse>(HttpResponseIden::WorkspaceId, wid, None)?;
// Sync and the CLI call this on a plain connection where each statement
// would otherwise commit on its own, leaving a partially-deleted workspace
// if one fails. A savepoint makes the cascade atomic there, and nests
// harmlessly inside the interactive path's transaction.
let conn = self.conn().resolve();
conn.execute_batch("SAVEPOINT delete_workspace")?;
let result: Result<Workspace> = (|| {
self.delete_many_untracked::<HttpResponseEvent>(
HttpResponseEventIden::WorkspaceId,
wid,
)?;
self.delete_many_untracked::<HttpResponse>(HttpResponseIden::WorkspaceId, wid)?;
self.delete_many_untracked::<HttpRequest>(HttpRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcEvent>(GrpcEventIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcConnection>(GrpcConnectionIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcRequest>(GrpcRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WebsocketEvent>(WebsocketEventIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WebsocketConnection>(
WebsocketConnectionIden::WorkspaceId,
wid,
)?;
self.delete_many_untracked::<WebsocketRequest>(WebsocketRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GraphQlIntrospection>(
GraphQlIntrospectionIden::WorkspaceId,
wid,
)?;
self.delete_many_untracked::<Folder>(FolderIden::WorkspaceId, wid)?;
self.delete_many_untracked::<Environment>(EnvironmentIden::WorkspaceId, wid)?;
self.delete_many_untracked::<CookieJar>(CookieJarIden::WorkspaceId, wid)?;
for import_source in self.list_import_sources(wid)? {
self.delete_import_source_resources(&import_source.id)?;
}
self.delete_many_untracked::<ImportSource>(ImportSourceIden::WorkspaceId, wid)?;
self.delete_many_untracked::<SyncState>(SyncStateIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WorkspaceMeta>(WorkspaceMetaIden::WorkspaceId, wid)?;
self.delete(workspace, source)
})();
let deleted = match result {
Ok(deleted) => {
conn.execute_batch("RELEASE delete_workspace")?;
deleted
}
Err(e) => {
let _ =
conn.execute_batch("ROLLBACK TO delete_workspace; RELEASE delete_workspace");
return Err(e);
}
};
// Best-effort cleanup of response bodies (disk files and blob chunks).
// Failures only orphan unreferenced data, and are logged.
let blob_ctx = blobs.connect();
for m in responses {
if let Some(p) = m.body_path {
if let Err(e) = std::fs::remove_file(&p) {
warn!("Failed to delete response body file {p:?}: {e}");
}
}
if let Err(e) = blob_ctx.delete_chunks_like(&format!("{}.%", m.id)) {
warn!("Failed to delete blobs for response {}: {e}", m.id);
}
}
Ok(deleted)
}
pub fn delete_workspace_by_id(
&self,
id: &str,
source: &UpdateSource,
blobs: &BlobManager,
) -> Result<Workspace> {
let workspace = self.get_workspace(id)?;
self.delete_workspace(&workspace, source, blobs)
}
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
self.upsert(w, source)
} }
pub fn resolve_auth_for_workspace( pub fn resolve_auth_for_workspace(
@@ -190,6 +78,100 @@ impl<'a> ClientDb<'a> {
} }
} }
impl<'a> WriteDb<'a> {
/// There is always at least one workspace. Called at startup and after a
/// workspace is deleted.
pub fn ensure_default_workspace(&self) -> Result<()> {
if self.find_all::<Workspace>()?.is_empty() {
self.upsert_workspace(
&Workspace { name: "Yaak".to_string(), ..Default::default() },
&UpdateSource::Background,
)?;
}
Ok(())
}
/// Delete a workspace and everything in it.
///
/// Children are bulk-deleted with one statement per table and are NOT
/// individually recorded in model_changes or emitted as events — the single
/// workspace delete event implies the subtree (see [`ModelChangeEvent::Delete`]).
/// This keeps huge workspaces (thousands of requests) fast and avoids
/// flooding event consumers.
pub fn delete_workspace(
&self,
workspace: &Workspace,
source: &UpdateSource,
blobs: &BlobManager,
) -> Result<Workspace> {
let wid = workspace.id.as_str();
// Collect response cleanup targets before their rows disappear. The actual
// cleanup runs at the end: response bodies live on disk and in the blob DB,
// which don't participate in this transaction, so removing them must wait
// until every statement that could fail (and roll back the rows) is done.
let responses = self.find_many::<HttpResponse>(HttpResponseIden::WorkspaceId, wid, None)?;
self.delete_many_untracked::<HttpResponseEvent>(HttpResponseEventIden::WorkspaceId, wid)?;
self.delete_many_untracked::<HttpResponse>(HttpResponseIden::WorkspaceId, wid)?;
self.delete_many_untracked::<HttpRequest>(HttpRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcEvent>(GrpcEventIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcConnection>(GrpcConnectionIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GrpcRequest>(GrpcRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WebsocketEvent>(WebsocketEventIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WebsocketConnection>(
WebsocketConnectionIden::WorkspaceId,
wid,
)?;
self.delete_many_untracked::<WebsocketRequest>(WebsocketRequestIden::WorkspaceId, wid)?;
self.delete_many_untracked::<GraphQlIntrospection>(
GraphQlIntrospectionIden::WorkspaceId,
wid,
)?;
self.delete_many_untracked::<Folder>(FolderIden::WorkspaceId, wid)?;
self.delete_many_untracked::<Environment>(EnvironmentIden::WorkspaceId, wid)?;
self.delete_many_untracked::<CookieJar>(CookieJarIden::WorkspaceId, wid)?;
for import_source in self.list_import_sources(wid)? {
self.delete_import_source_resources(&import_source.id)?;
}
self.delete_many_untracked::<ImportSource>(ImportSourceIden::WorkspaceId, wid)?;
self.delete_many_untracked::<SyncState>(SyncStateIden::WorkspaceId, wid)?;
self.delete_many_untracked::<WorkspaceMeta>(WorkspaceMetaIden::WorkspaceId, wid)?;
let deleted = self.delete(workspace, source)?;
self.ensure_default_workspace()?;
// Best-effort cleanup of response bodies (disk files and blob chunks).
// Failures only orphan unreferenced data, and are logged.
for m in responses {
if let Some(p) = m.body_path {
if let Err(e) = std::fs::remove_file(&p) {
warn!("Failed to delete response body file {p:?}: {e}");
}
}
let pattern = format!("{}.%", m.id);
if let Err(e) = blobs.with_tx(|b| b.delete_chunks_like(&pattern)) {
warn!("Failed to delete blobs for response {}: {e}", m.id);
}
}
Ok(deleted)
}
pub fn delete_workspace_by_id(
&self,
id: &str,
source: &UpdateSource,
blobs: &BlobManager,
) -> Result<Workspace> {
let workspace = self.get_workspace(id)?;
self.delete_workspace(&workspace, source, blobs)
}
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
self.upsert(w, source)
}
}
/// Global default headers that are always sent with requests unless overridden. /// Global default headers that are always sent with requests unless overridden.
/// These are prepended to the inheritance chain so workspace/folder/request headers /// These are prepended to the inheritance chain so workspace/folder/request headers
/// can override or disable them. /// can override or disable them.
+44 -27
View File
@@ -1,65 +1,82 @@
use crate::client_db::ClientDb; use crate::client_db::{ClientDb, WriteDb};
use crate::error::Error::GenericError; use crate::error::Error::GenericError;
use crate::util::ModelPayload; use crate::util::ModelPayload;
use rusqlite::{Transaction, TransactionBehavior}; use rusqlite::{Transaction, TransactionBehavior};
use std::sync::mpsc; use std::sync::mpsc;
use yaak_database::{ConnectionOrTx, DbContext, SqlitePool}; use yaak_database::{ConnectionOrTx, DbContext, SqlitePool};
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the /// Reads come from a pool; writes go through one connection.
// blocking `get()` serializes every DB access behind the slowest waiter, freezing the ///
// whole app whenever the pool is exhausted. /// SQLite in WAL mode lets many readers run alongside a single writer, and
/// never more than one writer. A second in-process writer can only wait, and
/// while it waits in the busy handler it sleeps, retries, and keeps its pool
/// slot. Enough of those and the pool is full of writers that are all asleep,
/// and every read in the app queues behind them. Giving writes exactly one
/// connection turns that into a plain queue: the next write starts the moment
/// the previous one commits, and it never takes a slot a read could use.
///
/// The pools are internally synchronized — don't wrap them in a Mutex. A Mutex
/// held across the blocking `get()` serializes every DB access behind the
/// slowest waiter.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct QueryManager { pub struct QueryManager {
pool: SqlitePool, readers: SqlitePool,
writer: SqlitePool,
events_tx: mpsc::Sender<ModelPayload>, events_tx: mpsc::Sender<ModelPayload>,
} }
impl QueryManager { impl QueryManager {
pub fn new(pool: SqlitePool, events_tx: mpsc::Sender<ModelPayload>) -> Self { /// `writer` must be a pool with a single connection; see [`crate::init_standalone`].
QueryManager { pool, events_tx } pub fn new(
readers: SqlitePool,
writer: SqlitePool,
events_tx: mpsc::Sender<ModelPayload>,
) -> Self {
QueryManager { readers, writer, events_tx }
} }
/// A read handle from the reader pool.
pub fn connect(&self) -> ClientDb<'_> { pub fn connect(&self) -> ClientDb<'_> {
let conn = self.pool.get().expect("Failed to get a new DB connection from the pool"); let conn = self.readers.get().expect("Failed to get a new DB connection from the pool");
let ctx = DbContext::new(ConnectionOrTx::Connection(conn)); ClientDb::new(DbContext::new(ConnectionOrTx::Connection(conn)))
ClientDb::new(ctx, self.events_tx.clone())
}
pub fn with_conn<F, T>(&self, func: F) -> T
where
F: FnOnce(&ClientDb) -> T,
{
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());
func(&db)
} }
/// Run `func` in a transaction on the writer connection.
///
/// Waits for any write in progress to commit first, and fails with a pool
/// error if that takes longer than the pool's timeout. Do not call this
/// from inside another `with_tx` closure: the inner call would wait for
/// the outer transaction, which is waiting on it.
///
/// Model events for the writes are sent once the transaction commits.
pub fn with_tx<T, E>( pub fn with_tx<T, E>(
&self, &self,
func: impl FnOnce(&ClientDb) -> std::result::Result<T, E>, func: impl FnOnce(&WriteDb) -> std::result::Result<T, E>,
) -> std::result::Result<T, E> ) -> std::result::Result<T, E>
where where
E: From<crate::error::Error>, E: From<crate::error::Error>,
{ {
let conn = self.pool.get().expect("Failed to get new DB connection from the pool"); let conn = self.writer.get().map_err(crate::error::Error::SqlPoolError)?;
// `new_unchecked` takes `&Connection`; see yaak_database::pool for why // `new_unchecked` takes `&Connection`; see yaak_database::pool for why
// the pool never hands out `&mut`. // the pool never hands out `&mut`.
let tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate) let tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)
.expect("Failed to start DB transaction"); .map_err(crate::error::Error::SqlError)?;
let ctx = DbContext::new(ConnectionOrTx::Transaction(&tx)); let db =
let db = ClientDb::new(ctx, self.events_tx.clone()); WriteDb::new(DbContext::new(ConnectionOrTx::Transaction(&tx)), self.events_tx.clone());
match func(&db) { match func(&db) {
Ok(val) => { Ok(val) => {
let events = db.into_events();
tx.commit() tx.commit()
.map_err(|e| GenericError(format!("Failed to commit transaction {e:?}")))?; .map_err(|e| GenericError(format!("Failed to commit transaction {e:?}")))?;
for payload in events {
let _ = self.events_tx.send(payload);
}
Ok(val) Ok(val)
} }
Err(e) => { Err(e) => {
drop(db);
tx.rollback() tx.rollback()
.map_err(|e| GenericError(format!("Failed to rollback transaction {e:?}")))?; .map_err(|e| GenericError(format!("Failed to rollback transaction {e:?}")))?;
Err(e) Err(e)
+1 -1
View File
@@ -277,7 +277,7 @@ pub fn get_workspace_export_resources(
data.resources.workspaces.push(db.find_one(WorkspaceIden::Id, workspace_id)?); data.resources.workspaces.push(db.find_one(WorkspaceIden::Id, workspace_id)?);
data.resources.environments.append( data.resources.environments.append(
&mut db &mut db
.list_environments_ensure_base(workspace_id)? .list_environments(workspace_id)?
.into_iter() .into_iter()
.filter(|e| include_private_environments || e.public) .filter(|e| include_private_environments || e.public)
.collect(), .collect(),
+4 -10
View File
@@ -24,11 +24,7 @@ pub async fn delete_and_uninstall(
Some(label) => UpdateSource::from_window_label(label), Some(label) => UpdateSource::from_window_label(label),
None => UpdateSource::Background, None => UpdateSource::Background,
}; };
// Scope the db connection so it doesn't live across await let plugin = query_manager.with_tx(|db| db.delete_plugin_by_id(plugin_id, &update_source))?;
let plugin = {
let db = query_manager.connect();
db.delete_plugin_by_id(plugin_id, &update_source)?
};
if let Err(err) = plugin_manager.uninstall(plugin_context, plugin.directory.as_str()).await { if let Err(err) = plugin_manager.uninstall(plugin_context, plugin.directory.as_str()).await {
if !matches!(err, PluginNotFoundErr(_)) { if !matches!(err, PluginNotFoundErr(_)) {
return Err(err); return Err(err);
@@ -72,9 +68,7 @@ pub async fn download_and_install(
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
// Scope the db connection so it doesn't live across await let plugin = query_manager.with_tx(|db| {
let plugin = {
let db = query_manager.connect();
db.upsert_plugin( db.upsert_plugin(
&Plugin { &Plugin {
id: plugin_version.id.clone(), id: plugin_version.id.clone(),
@@ -86,8 +80,8 @@ pub async fn download_and_install(
..Default::default() ..Default::default()
}, },
&UpdateSource::Background, &UpdateSource::Background,
)? )
}; })?;
plugin_manager.add_plugin(plugin_context, &plugin).await?; plugin_manager.add_plugin(plugin_context, &plugin).await?;
+3 -5
View File
@@ -187,9 +187,7 @@ impl PluginManager {
} }
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?; let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
// Scope the db connection so the future stays Send across the await below let plugins = query_manager.with_tx(|db| {
let plugins = {
let db = query_manager.connect();
for dir in &bundled_dirs { for dir in &bundled_dirs {
if db.get_plugin_by_directory(dir).is_none() { if db.get_plugin_by_directory(dir).is_none() {
db.upsert_plugin( db.upsert_plugin(
@@ -204,8 +202,8 @@ impl PluginManager {
)?; )?;
} }
} }
db.list_plugins()? db.list_plugins()
}; })?;
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await; let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
if !init_errors.is_empty() { if !init_errors.is_empty() {
+81 -50
View File
@@ -11,8 +11,11 @@ use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use ts_rs::TS; use ts_rs::TS;
use yaak_models::blob_manager::BlobManager; use yaak_models::blob_manager::BlobManager;
use yaak_models::client_db::ClientDb; use yaak_models::client_db::{ClientDb, WriteDb};
use yaak_models::models::{SyncState, WorkspaceMeta}; use yaak_models::models::{
Environment, Folder, GrpcRequest, HttpRequest, SyncState, WebsocketRequest, Workspace,
WorkspaceMeta,
};
use yaak_models::util::{UpdateSource, get_workspace_export_resources}; use yaak_models::util::{UpdateSource, get_workspace_export_resources};
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
@@ -336,17 +339,40 @@ fn workspace_models(db: &ClientDb, version: &str, workspace_id: &str) -> Result<
Ok(sync_models) Ok(sync_models)
} }
/// Apply sync operations to the filesystem and database. /// The database half of a sync apply, ready to run once the files are on disk.
/// Returns a list of SyncStateOps that should be applied afterward. pub struct PendingDbSyncOps {
pub fn apply_sync_ops( sync_state_ops: Vec<SyncStateOp>,
db: &ClientDb, deletes: Vec<SyncModel>,
blobs: &BlobManager, workspaces: Vec<Workspace>,
environments: Vec<Environment>,
folders: Vec<Folder>,
http_requests: Vec<HttpRequest>,
grpc_requests: Vec<GrpcRequest>,
websocket_requests: Vec<WebsocketRequest>,
}
/// Apply the filesystem half of the sync operations: create, rewrite and
/// delete files. Returns the database half, for [`apply_db_sync_ops`].
///
/// Split this way so the file work, which can be slow, happens before the
/// write transaction is opened rather than inside it.
pub fn apply_fs_sync_ops(
workspace_id: &str, workspace_id: &str,
sync_dir: &Path, sync_dir: &Path,
sync_ops: Vec<SyncOp>, sync_ops: Vec<SyncOp>,
) -> Result<Vec<SyncStateOp>> { ) -> Result<PendingDbSyncOps> {
let mut pending = PendingDbSyncOps {
sync_state_ops: Vec::new(),
deletes: Vec::new(),
workspaces: Vec::new(),
environments: Vec::new(),
folders: Vec::new(),
http_requests: Vec::new(),
grpc_requests: Vec::new(),
websocket_requests: Vec::new(),
};
if sync_ops.is_empty() { if sync_ops.is_empty() {
return Ok(Vec::new()); return Ok(pending);
} }
info!( info!(
@@ -354,21 +380,13 @@ pub fn apply_sync_ops(
sync_ops.iter().map(|op| op.to_string()).collect::<Vec<String>>().join(", ") sync_ops.iter().map(|op| op.to_string()).collect::<Vec<String>>().join(", ")
); );
let mut sync_state_ops = Vec::new();
let mut workspaces_to_upsert = Vec::new();
let mut environments_to_upsert = Vec::new();
let mut folders_to_upsert = Vec::new();
let mut http_requests_to_upsert = Vec::new();
let mut grpc_requests_to_upsert = Vec::new();
let mut websocket_requests_to_upsert = Vec::new();
for op in sync_ops { for op in sync_ops {
// Only apply things if workspace ID matches // Only apply things if workspace ID matches
if op.workspace_id() != workspace_id { if op.workspace_id() != workspace_id {
continue; continue;
} }
sync_state_ops.push(match op { let state_op = match op {
SyncOp::FsCreate { model } => { SyncOp::FsCreate { model } => {
let rel_path = derive_model_filename(&model); let rel_path = derive_model_filename(&model);
let abs_path = sync_dir.join(rel_path.clone()); let abs_path = sync_dir.join(rel_path.clone());
@@ -402,17 +420,7 @@ pub fn apply_sync_ops(
}, },
SyncOp::DbCreate { fs } => { SyncOp::DbCreate { fs } => {
let model_id = fs.model.id(); let model_id = fs.model.id();
pending.push_upsert(fs.model);
// Push updates to arrays so we can do them all in a single
// batch upsert to make foreign keys happy
match fs.model {
SyncModel::Environment(m) => environments_to_upsert.push(m),
SyncModel::Folder(m) => folders_to_upsert.push(m),
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
};
SyncStateOp::Create { SyncStateOp::Create {
model_id, model_id,
checksum: fs.checksum.to_owned(), checksum: fs.checksum.to_owned(),
@@ -420,16 +428,7 @@ pub fn apply_sync_ops(
} }
} }
SyncOp::DbUpdate { state, fs } => { SyncOp::DbUpdate { state, fs } => {
// Push updates to arrays so we can do them all in a single pending.push_upsert(fs.model);
// batch upsert to make foreign keys happy
match fs.model {
SyncModel::Environment(m) => environments_to_upsert.push(m),
SyncModel::Folder(m) => folders_to_upsert.push(m),
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
}
SyncStateOp::Update { SyncStateOp::Update {
state: state.to_owned(), state: state.to_owned(),
checksum: fs.checksum.to_owned(), checksum: fs.checksum.to_owned(),
@@ -437,20 +436,52 @@ pub fn apply_sync_ops(
} }
} }
SyncOp::DbDelete { model, state } => { SyncOp::DbDelete { model, state } => {
delete_model(db, blobs, &model)?; pending.deletes.push(model);
SyncStateOp::Delete { state: state.to_owned() } SyncStateOp::Delete { state: state.to_owned() }
} }
SyncOp::IgnorePrivate { .. } => SyncStateOp::NoOp, SyncOp::IgnorePrivate { .. } => SyncStateOp::NoOp,
}); };
pending.sync_state_ops.push(state_op);
}
Ok(pending)
}
impl PendingDbSyncOps {
/// Upserts are collected per model type and written in one batch so
/// foreign keys are satisfied.
fn push_upsert(&mut self, model: SyncModel) {
match model {
SyncModel::Environment(m) => self.environments.push(m),
SyncModel::Folder(m) => self.folders.push(m),
SyncModel::GrpcRequest(m) => self.grpc_requests.push(m),
SyncModel::HttpRequest(m) => self.http_requests.push(m),
SyncModel::WebsocketRequest(m) => self.websocket_requests.push(m),
SyncModel::Workspace(m) => self.workspaces.push(m),
}
}
}
/// Apply the database half of the sync operations.
/// Returns a list of SyncStateOps that should be applied afterward.
pub fn apply_db_sync_ops(
db: &WriteDb,
blobs: &BlobManager,
workspace_id: &str,
sync_dir: &Path,
pending: PendingDbSyncOps,
) -> Result<Vec<SyncStateOp>> {
for model in &pending.deletes {
delete_model(db, blobs, model)?;
} }
let upserted_models = db.batch_upsert( let upserted_models = db.batch_upsert(
workspaces_to_upsert, pending.workspaces,
environments_to_upsert, pending.environments,
folders_to_upsert, pending.folders,
http_requests_to_upsert, pending.http_requests,
grpc_requests_to_upsert, pending.grpc_requests,
websocket_requests_to_upsert, pending.websocket_requests,
&UpdateSource::Sync, &UpdateSource::Sync,
)?; )?;
@@ -482,7 +513,7 @@ pub fn apply_sync_ops(
}?; }?;
} }
Ok(sync_state_ops) Ok(pending.sync_state_ops)
} }
#[derive(Debug)] #[derive(Debug)]
@@ -504,7 +535,7 @@ pub enum SyncStateOp {
} }
pub fn apply_sync_state_ops( pub fn apply_sync_state_ops(
db: &ClientDb, db: &WriteDb,
workspace_id: &str, workspace_id: &str,
sync_dir: &Path, sync_dir: &Path,
ops: Vec<SyncStateOp>, ops: Vec<SyncStateOp>,
@@ -549,7 +580,7 @@ fn derive_model_filename(m: &SyncModel) -> PathBuf {
Path::new(&rel).to_path_buf() Path::new(&rel).to_path_buf()
} }
fn delete_model(db: &ClientDb, blobs: &BlobManager, model: &SyncModel) -> Result<()> { fn delete_model(db: &WriteDb, blobs: &BlobManager, model: &SyncModel) -> Result<()> {
match model { match model {
SyncModel::Workspace(m) => { SyncModel::Workspace(m) => {
db.delete_workspace(&m, &UpdateSource::Sync, blobs)?; db.delete_workspace(&m, &UpdateSource::Sync, blobs)?;
+50 -26
View File
@@ -104,7 +104,7 @@ pub async fn boot() -> Result<()> {
let (queries, blobs, events) = let (queries, blobs, events) =
yaak_models::init_standalone(DB_NAME, BLOB_DB_NAME).map_err(js_error)?; yaak_models::init_standalone(DB_NAME, BLOB_DB_NAME).map_err(js_error)?;
if let Err(e) = yaak_lifecycle::on_launch(&lifecycle_host(), &queries.connect(), &blobs) { if let Err(e) = queries.with_tx(|tx| yaak_lifecycle::on_launch(&lifecycle_host(), tx, &blobs)) {
web_sys::console::warn_2(&"on_launch hook failed".into(), &js_error(e)); web_sys::console::warn_2(&"on_launch hook failed".into(), &js_error(e));
} }
@@ -266,10 +266,17 @@ fn dispatch(
if let Some(wid) = req.workspace_id.as_deref() { if let Some(wid) = req.workspace_id.as_deref() {
let e = js_error; let e = js_error;
// Opening a workspace is where the rows it is assumed to have get created
host.queries
.with_tx(|tx| {
tx.ensure_base_environment(wid)?;
tx.ensure_default_cookie_jar(wid)?;
tx.ensure_workspace_meta(wid)?;
Ok::<(), yaak_models::error::Error>(())
})
.map_err(e)?;
list.extend(db.list_cookie_jars(wid).map_err(e)?.into_iter().map(Into::into)); list.extend(db.list_cookie_jars(wid).map_err(e)?.into_iter().map(Into::into));
list.extend( list.extend(db.list_environments(wid).map_err(e)?.into_iter().map(Into::into));
db.list_environments_ensure_base(wid).map_err(e)?.into_iter().map(Into::into),
);
list.extend(db.list_folders(wid).map_err(e)?.into_iter().map(Into::into)); list.extend(db.list_folders(wid).map_err(e)?.into_iter().map(Into::into));
list.extend(db.list_grpc_connections(wid).map_err(e)?.into_iter().map(Into::into)); list.extend(db.list_grpc_connections(wid).map_err(e)?.into_iter().map(Into::into));
list.extend(db.list_grpc_requests(wid).map_err(e)?.into_iter().map(Into::into)); list.extend(db.list_grpc_requests(wid).map_err(e)?.into_iter().map(Into::into));
@@ -291,9 +298,10 @@ fn dispatch(
"models_upsert" => { "models_upsert" => {
let req: ModelReq = from_js(payload)?; let req: ModelReq = from_js(payload)?;
let db = host.queries.connect(); let id = host
let id = .queries
models_ops::upsert_model(&db, &host.blobs, req.model, source).map_err(js_error)?; .with_tx(|tx| models_ops::upsert_model(tx, &host.blobs, req.model, source))
.map_err(js_error)?;
to_json(id) to_json(id)
} }
@@ -330,13 +338,14 @@ fn dispatch(
let req: UpsertIntrospectionReq = from_js(payload)?; let req: UpsertIntrospectionReq = from_js(payload)?;
let saved = host let saved = host
.queries .queries
.connect() .with_tx(|tx| {
.upsert_graphql_introspection( tx.upsert_graphql_introspection(
&req.workspace_id, &req.workspace_id,
&req.request_id, &req.request_id,
req.content, req.content,
source, source,
) )
})
.map_err(js_error)?; .map_err(js_error)?;
to_json(saved) to_json(saved)
} }
@@ -366,10 +375,14 @@ fn dispatch(
if req.before == req.after { if req.before == req.after {
return to_json(()); return to_json(());
} }
let db = host.queries.connect(); host.queries
let jar = db.get_cookie_jar(&req.cookie_jar_id).map_err(js_error)?; .with_tx(|tx| {
let cookies = apply_cookie_changes(jar.cookies.clone(), &req.before, &req.after); let jar = tx.get_cookie_jar(&req.cookie_jar_id)?;
db.upsert_cookie_jar(&CookieJar { cookies, ..jar }, source).map_err(js_error)?; let cookies =
apply_cookie_changes(jar.cookies.clone(), &req.before, &req.after);
tx.upsert_cookie_jar(&CookieJar { cookies, ..jar }, source)
})
.map_err(js_error)?;
to_json(()) to_json(())
} }
@@ -378,26 +391,34 @@ fn dispatch(
// writes fan out to every tab as `model_writes` like any other. // writes fan out to every tab as `model_writes` like any other.
"web_insert_http_response_events" => { "web_insert_http_response_events" => {
let req: InsertResponseEventsReq = from_js(payload)?; let req: InsertResponseEventsReq = from_js(payload)?;
let db = host.queries.connect(); host.queries
.with_tx(|tx| {
for event in req.events { for event in req.events {
let model = HttpResponseEvent::new(&req.response_id, &req.workspace_id, event); let model =
db.upsert_http_response_event(&model, source).map_err(js_error)?; HttpResponseEvent::new(&req.response_id, &req.workspace_id, event);
tx.upsert_http_response_event(&model, source)?;
} }
Ok::<(), yaak_models::error::Error>(())
})
.map_err(js_error)?;
to_json(()) to_json(())
} }
"cmd_get_workspace_meta" => { "cmd_get_workspace_meta" => {
let req: WorkspaceIdReq = from_js(payload)?; let req: WorkspaceIdReq = from_js(payload)?;
let db = host.queries.connect(); let workspace =
let workspace = db.get_workspace(&req.workspace_id).map_err(js_error)?; host.queries.connect().get_workspace(&req.workspace_id).map_err(js_error)?;
to_json(db.get_or_create_workspace_meta(&workspace.id).map_err(js_error)?) to_json(
host.queries
.with_tx(|tx| tx.ensure_workspace_meta(&workspace.id))
.map_err(js_error)?,
)
} }
"cmd_delete_all_http_responses" => { "cmd_delete_all_http_responses" => {
let req: RequestIdReq = from_js(payload)?; let req: RequestIdReq = from_js(payload)?;
host.queries host.queries
.connect() .with_tx(|tx| tx.delete_all_http_responses_for_request(&req.request_id, source))
.delete_all_http_responses_for_request(&req.request_id, source)
.map_err(js_error)?; .map_err(js_error)?;
to_json(()) to_json(())
} }
@@ -569,16 +590,19 @@ pub fn blob_get(id: &str) -> Result<Option<Vec<u8>>> {
pub fn blob_put(id: &str, bytes: &[u8]) -> Result<()> { pub fn blob_put(id: &str, bytes: &[u8]) -> Result<()> {
const CHUNK: usize = 512 * 1024; const CHUNK: usize = 512 * 1024;
with_host(|host| { with_host(|host| {
let ctx = host.blobs.connect(); host.blobs
ctx.delete_chunks(id).map_err(js_error)?; .with_tx(|b| {
b.delete_chunks(id)?;
for (i, part) in bytes.chunks(CHUNK).enumerate() { for (i, part) in bytes.chunks(CHUNK).enumerate() {
ctx.insert_chunk(&BodyChunk::new(id, i as i32, part.to_vec())).map_err(js_error)?; b.insert_chunk(&BodyChunk::new(id, i as i32, part.to_vec()))?;
} }
Ok(()) Ok::<(), yaak_models::error::Error>(())
})
.map_err(js_error)
}) })
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn blob_delete(id: &str) -> Result<()> { pub fn blob_delete(id: &str) -> Result<()> {
with_host(|host| host.blobs.connect().delete_chunks(id).map_err(js_error)) with_host(|host| host.blobs.with_tx(|b| b.delete_chunks(id)).map_err(js_error))
} }
+48 -58
View File
@@ -4,7 +4,7 @@ use log::info;
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use yaak_models::client_db::ClientDb; use yaak_models::client_db::{ClientDb, WriteDb};
use yaak_models::models::{ use yaak_models::models::{
AnyModel, DEFAULT_REQUEST_MESSAGE_SIZE, Environment, Folder, GrpcRequest, HttpRequest, AnyModel, DEFAULT_REQUEST_MESSAGE_SIZE, Environment, Folder, GrpcRequest, HttpRequest,
ImportSource, ImportSourceResource, UpsertModelInfo, WebsocketRequest, Workspace, ImportSource, ImportSourceResource, UpsertModelInfo, WebsocketRequest, Workspace,
@@ -351,7 +351,7 @@ pub fn commit_import_plan(
}) })
} }
fn commit_plan_in_tx(db: &ClientDb, plan: ImportPlan) -> Result<BatchUpsertResult> { fn commit_plan_in_tx(db: &WriteDb, plan: ImportPlan) -> Result<BatchUpsertResult> {
let items: BTreeMap<String, ImportPlanItem> = let items: BTreeMap<String, ImportPlanItem> =
plan.items.iter().map(|item| (item.model_id.clone(), item.clone())).collect(); plan.items.iter().map(|item| (item.model_id.clone(), item.clone())).collect();
@@ -460,7 +460,7 @@ fn commit_plan_in_tx(db: &ClientDb, plan: ImportPlan) -> Result<BatchUpsertResul
} }
/// A folder deletion may have already cascaded over the model, so absent models are skipped. /// A folder deletion may have already cascaded over the model, so absent models are skipped.
fn delete_existing_model(db: &ClientDb, resource: ImportResourceType, id: &str) -> Result<()> { fn delete_existing_model(db: &WriteDb, resource: ImportResourceType, id: &str) -> Result<()> {
use ImportResourceType::*; use ImportResourceType::*;
let source = &UpdateSource::Import; let source = &UpdateSource::Import;
match resource { match resource {
@@ -503,7 +503,7 @@ fn delete_existing_model(db: &ClientDb, resource: ImportResourceType, id: &str)
/// offered again next time. A resource the user turned down is remembered as a row without a /// offered again next time. A resource the user turned down is remembered as a row without a
/// model, so it is neither re-offered nor resurrected. /// model, so it is neither re-offered nor resurrected.
fn record_import_source( fn record_import_source(
db: &ClientDb, db: &WriteDb,
plan: &ImportPlan, plan: &ImportPlan,
items: &BTreeMap<String, ImportPlanItem>, items: &BTreeMap<String, ImportPlanItem>,
upserted: &BatchUpsertResult, upserted: &BatchUpsertResult,
@@ -1619,13 +1619,10 @@ mod tests {
name: "Selected Folder".to_string(), name: "Selected Folder".to_string(),
..Default::default() ..Default::default()
}; };
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
destination = db destination = db.upsert_workspace(&destination, &UpdateSource::Import)?;
.upsert_workspace(&destination, &UpdateSource::Import) db.upsert_folder(&selected_folder, &UpdateSource::Import)?;
.expect("create destination");
db.upsert_folder(&selected_folder, &UpdateSource::Import)
.expect("create selected folder");
db.upsert_environment( db.upsert_environment(
&Environment { &Environment {
id: "ev_destination_base".to_string(), id: "ev_destination_base".to_string(),
@@ -1643,8 +1640,9 @@ mod tests {
}, },
&UpdateSource::Import, &UpdateSource::Import,
) )
.expect("create base environment"); })
} .expect("seed destination");
let workspace_count = query_manager.connect().list_workspaces().expect("list").len();
let plan = plan_import_resources( let plan = plan_import_resources(
&query_manager, &query_manager,
@@ -1662,13 +1660,10 @@ mod tests {
// Planning performed only reads. // Planning performed only reads.
{ {
let db = query_manager.connect(); let db = query_manager.connect();
assert_eq!(db.list_workspaces().expect("list workspaces").len(), 1); assert_eq!(db.list_workspaces().expect("list workspaces").len(), workspace_count);
assert_eq!(db.list_folders(&destination.id).expect("list folders").len(), 1); assert_eq!(db.list_folders(&destination.id).expect("list folders").len(), 1);
assert!(db.list_http_requests(&destination.id).expect("list requests").is_empty()); assert!(db.list_http_requests(&destination.id).expect("list requests").is_empty());
assert_eq!( assert_eq!(db.list_environments(&destination.id).expect("list environments").len(), 1);
db.list_environments_ensure_base(&destination.id).expect("list environments").len(),
1
);
assert_eq!(db.get_workspace(&destination.id).expect("get destination"), destination); assert_eq!(db.get_workspace(&destination.id).expect("get destination"), destination);
} }
@@ -1861,8 +1856,7 @@ mod tests {
yaak_models::init_in_memory().expect("initialize database"); yaak_models::init_in_memory().expect("initialize database");
let destination = destination_workspace(); let destination = destination_workspace();
query_manager query_manager
.connect() .with_tx(|tx| tx.upsert_workspace(&destination, &UpdateSource::Import))
.upsert_workspace(&destination, &UpdateSource::Import)
.expect("create destination"); .expect("create destination");
let resources = ImportResources { let resources = ImportResources {
workspaces: vec![ workspaces: vec![
@@ -2247,10 +2241,7 @@ mod tests {
let db = query_manager.connect(); let db = query_manager.connect();
assert_eq!(db.list_http_requests(&workspace_id).expect("list requests").len(), 2); assert_eq!(db.list_http_requests(&workspace_id).expect("list requests").len(), 2);
assert_eq!(db.list_folders(&workspace_id).expect("list folders").len(), 1); assert_eq!(db.list_folders(&workspace_id).expect("list folders").len(), 1);
assert_eq!( assert_eq!(db.list_environments(&workspace_id).expect("list environments").len(), 1);
db.list_environments_ensure_base(&workspace_id).expect("list environments").len(),
1
);
let rows = db.list_import_source_resources(&source.id).expect("list resource rows"); let rows = db.list_import_source_resources(&source.id).expect("list resource rows");
assert_eq!(rows.len(), 4, "re-commit replaces rows instead of accumulating"); assert_eq!(rows.len(), 4, "re-commit replaces rows instead of accumulating");
} }
@@ -2269,11 +2260,10 @@ mod tests {
.id .id
.clone(); .clone();
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let nested = db let nested = db
.list_http_requests(&workspace_id) .list_http_requests(&workspace_id)?
.expect("list requests")
.into_iter() .into_iter()
.find(|r| r.name == "Nested Request") .find(|r| r.name == "Nested Request")
.expect("nested request"); .expect("nested request");
@@ -2284,8 +2274,8 @@ mod tests {
}, },
&UpdateSource::Background, &UpdateSource::Background,
) )
})
.expect("edit nested request locally"); .expect("edit nested request locally");
}
let mut resources = imported_resources(); let mut resources = imported_resources();
resources.http_requests[0].url = "https://example.com/root-v2".to_string(); resources.http_requests[0].url = "https://example.com/root-v2".to_string();
@@ -2370,15 +2360,15 @@ mod tests {
.id .id
.clone(); .clone();
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let root = db.get_http_request(&root_id).expect("get root"); let root = db.get_http_request(&root_id)?;
db.upsert_http_request( db.upsert_http_request(
&HttpRequest { url: "https://example.com/root-local".to_string(), ..root }, &HttpRequest { url: "https://example.com/root-local".to_string(), ..root },
&UpdateSource::Background, &UpdateSource::Background,
) )
})
.expect("edit root locally"); .expect("edit root locally");
}
let mut resources = imported_resources(); let mut resources = imported_resources();
resources.http_requests[0].url = "https://example.com/root-v2".to_string(); resources.http_requests[0].url = "https://example.com/root-v2".to_string();
@@ -2507,8 +2497,7 @@ mod tests {
.clone(); .clone();
query_manager query_manager
.connect() .with_tx(|tx| tx.delete_http_request_by_id(&root_id, &UpdateSource::Background))
.delete_http_request_by_id(&root_id, &UpdateSource::Background)
.expect("delete root locally"); .expect("delete root locally");
let plan = replan(&query_manager, &workspace_id, imported_resources()); let plan = replan(&query_manager, &workspace_id, imported_resources());
@@ -2703,10 +2692,9 @@ mod tests {
let workspace_id = committed.workspaces[0].id.clone(); let workspace_id = committed.workspaces[0].id.clone();
// A second source claiming the same keys leaves nothing to merge into safely. // A second source claiming the same keys leaves nothing to merge into safely.
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let other = db let other = db.upsert_import_source(
.upsert_import_source(
&ImportSource { &ImportSource {
workspace_id: workspace_id.clone(), workspace_id: workspace_id.clone(),
importer: "OpenAPI".to_string(), importer: "OpenAPI".to_string(),
@@ -2715,18 +2703,18 @@ mod tests {
..Default::default() ..Default::default()
}, },
&UpdateSource::Import, &UpdateSource::Import,
) )?;
.expect("create second source");
for key in ["env:base", "folder:src", "op:root", "op:nested"] { for key in ["env:base", "folder:src", "op:root", "op:nested"] {
db.upsert_import_source_resource(&ImportSourceResource { db.upsert_import_source_resource(&ImportSourceResource {
import_source_id: other.id.clone(), import_source_id: other.id.clone(),
source_key: key.to_string(), source_key: key.to_string(),
model_type: "http_request".to_string(), model_type: "http_request".to_string(),
..Default::default() ..Default::default()
})?;
}
Ok::<_, yaak_models::error::Error>(())
}) })
.expect("claim the same keys"); .expect("claim the same keys");
}
}
let third = let third =
ImportOrigin { origin: "/tmp/third.yaml".to_string(), label: "third.yaml".to_string() }; ImportOrigin { origin: "/tmp/third.yaml".to_string(), label: "third.yaml".to_string() };
@@ -2821,12 +2809,11 @@ mod tests {
let committed = first_import(&query_manager); let committed = first_import(&query_manager);
let workspace_id = committed.workspaces[0].id.clone(); let workspace_id = committed.workspaces[0].id.clone();
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let sources = db.list_import_sources(&workspace_id).expect("list import sources"); let sources = db.list_import_sources(&workspace_id)?;
let row = db let row = db
.list_import_source_resources(&sources[0].id) .list_import_source_resources(&sources[0].id)?
.expect("list rows")
.into_iter() .into_iter()
.find(|r| r.source_key == "op:root") .find(|r| r.source_key == "op:root")
.expect("row for the root request"); .expect("row for the root request");
@@ -2834,8 +2821,8 @@ mod tests {
content_hash: Some("v99:from-the-future".to_string()), content_hash: Some("v99:from-the-future".to_string()),
..row ..row
}) })
})
.expect("write an unreadable hash"); .expect("write an unreadable hash");
}
let plan = replan(&query_manager, &workspace_id, imported_resources()); let plan = replan(&query_manager, &workspace_id, imported_resources());
assert_eq!( assert_eq!(
@@ -2874,17 +2861,20 @@ mod tests {
.clone(); .clone();
// Opening the request in the editor stamps a row ID onto every header it renders. // Opening the request in the editor stamps a row ID onto every header it renders.
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let root = db.get_http_request(&root_id).expect("get root"); let root = db.get_http_request(&root_id)?;
let headers = root let headers = root
.headers .headers
.iter() .iter()
.map(|h| HttpRequestHeader { id: Some("hd_generated".to_string()), ..h.clone() }) .map(|h| HttpRequestHeader {
id: Some("hd_generated".to_string()),
..h.clone()
})
.collect(); .collect();
db.upsert_http_request(&HttpRequest { headers, ..root }, &UpdateSource::Background) db.upsert_http_request(&HttpRequest { headers, ..root }, &UpdateSource::Background)
})
.expect("stamp row ids"); .expect("stamp row ids");
}
let plan = replan(&query_manager, &workspace_id, resources); let plan = replan(&query_manager, &workspace_id, resources);
assert_eq!( assert_eq!(
@@ -2997,15 +2987,15 @@ mod tests {
.id .id
.clone(); .clone();
{ query_manager
let db = query_manager.connect(); .with_tx(|db| {
let root = db.get_http_request(&root_id).expect("get root"); let root = db.get_http_request(&root_id)?;
db.upsert_http_request( db.upsert_http_request(
&HttpRequest { url: "https://example.com/root-local".to_string(), ..root }, &HttpRequest { url: "https://example.com/root-local".to_string(), ..root },
&UpdateSource::Background, &UpdateSource::Background,
) )
})
.expect("edit root locally"); .expect("edit root locally");
}
let mut plan = replan(&query_manager, &workspace_id, imported_resources()); let mut plan = replan(&query_manager, &workspace_id, imported_resources());
let root = item_by_name(&plan, "Root Request"); let root = item_by_name(&plan, "Root Request");
+39 -35
View File
@@ -1,6 +1,7 @@
use crate::response_body::ResponseBodyStore; use crate::response_body::ResponseBodyStore;
use base64::Engine; use base64::Engine;
use base64::prelude::BASE64_STANDARD; use base64::prelude::BASE64_STANDARD;
use log::warn;
use yaak_models::models::AnyModel; use yaak_models::models::AnyModel;
use yaak_models::query_manager::QueryManager; use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -226,11 +227,18 @@ fn build_shared_reply(
InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }) InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })
} }
SharedRequest::SetKeyValue(req) => { SharedRequest::SetKeyValue(req) => {
query_manager.connect().set_plugin_key_value(context.plugin_name, &req.key, &req.value); if let Err(e) = query_manager.with_tx(|tx| {
tx.set_plugin_key_value(context.plugin_name, &req.key, &req.value);
Ok::<(), yaak_models::error::Error>(())
}) {
warn!("Failed to set plugin key value: {e}");
}
InternalEventPayload::SetKeyValueResponse(yaak_plugins::events::SetKeyValueResponse {}) InternalEventPayload::SetKeyValueResponse(yaak_plugins::events::SetKeyValueResponse {})
} }
SharedRequest::DeleteKeyValue(req) => { SharedRequest::DeleteKeyValue(req) => {
match query_manager.connect().delete_plugin_key_value(context.plugin_name, &req.key) { match query_manager
.with_tx(|tx| tx.delete_plugin_key_value(context.plugin_name, &req.key))
{
Ok(deleted) => { Ok(deleted) => {
InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }) InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted })
} }
@@ -331,7 +339,9 @@ fn build_shared_reply(
let model = match &req.model { let model = match &req.model {
HttpRequest(m) => { HttpRequest(m) => {
match query_manager.connect().upsert_http_request(m, &UpdateSource::Plugin) { match query_manager
.with_tx(|tx| tx.upsert_http_request(m, &UpdateSource::Plugin))
{
Ok(model) => HttpRequest(model), Ok(model) => HttpRequest(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -341,7 +351,9 @@ fn build_shared_reply(
} }
} }
GrpcRequest(m) => { GrpcRequest(m) => {
match query_manager.connect().upsert_grpc_request(m, &UpdateSource::Plugin) { match query_manager
.with_tx(|tx| tx.upsert_grpc_request(m, &UpdateSource::Plugin))
{
Ok(model) => GrpcRequest(model), Ok(model) => GrpcRequest(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -351,7 +363,8 @@ fn build_shared_reply(
} }
} }
WebsocketRequest(m) => { WebsocketRequest(m) => {
match query_manager.connect().upsert_websocket_request(m, &UpdateSource::Plugin) match query_manager
.with_tx(|tx| tx.upsert_websocket_request(m, &UpdateSource::Plugin))
{ {
Ok(model) => WebsocketRequest(model), Ok(model) => WebsocketRequest(model),
Err(err) => { Err(err) => {
@@ -362,7 +375,7 @@ fn build_shared_reply(
} }
} }
Folder(m) => { Folder(m) => {
match query_manager.connect().upsert_folder(m, &UpdateSource::Plugin) { match query_manager.with_tx(|tx| tx.upsert_folder(m, &UpdateSource::Plugin)) {
Ok(model) => Folder(model), Ok(model) => Folder(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -372,7 +385,9 @@ fn build_shared_reply(
} }
} }
Environment(m) => { Environment(m) => {
match query_manager.connect().upsert_environment(m, &UpdateSource::Plugin) { match query_manager
.with_tx(|tx| tx.upsert_environment(m, &UpdateSource::Plugin))
{
Ok(model) => Environment(model), Ok(model) => Environment(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -382,7 +397,8 @@ fn build_shared_reply(
} }
} }
Workspace(m) => { Workspace(m) => {
match query_manager.connect().upsert_workspace(m, &UpdateSource::Plugin) { match query_manager.with_tx(|tx| tx.upsert_workspace(m, &UpdateSource::Plugin))
{
Ok(model) => Workspace(model), Ok(model) => Workspace(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -404,8 +420,7 @@ fn build_shared_reply(
let model = match req.model.as_str() { let model = match req.model.as_str() {
"http_request" => { "http_request" => {
match query_manager match query_manager
.connect() .with_tx(|tx| tx.delete_http_request_by_id(&req.id, &UpdateSource::Plugin))
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)
{ {
Ok(model) => AnyModel::HttpRequest(model), Ok(model) => AnyModel::HttpRequest(model),
Err(err) => { Err(err) => {
@@ -417,8 +432,7 @@ fn build_shared_reply(
} }
"grpc_request" => { "grpc_request" => {
match query_manager match query_manager
.connect() .with_tx(|tx| tx.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin))
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)
{ {
Ok(model) => AnyModel::GrpcRequest(model), Ok(model) => AnyModel::GrpcRequest(model),
Err(err) => { Err(err) => {
@@ -429,10 +443,9 @@ fn build_shared_reply(
} }
} }
"websocket_request" => { "websocket_request" => {
match query_manager match query_manager.with_tx(|tx| {
.connect() tx.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin) }) {
{
Ok(model) => AnyModel::WebsocketRequest(model), Ok(model) => AnyModel::WebsocketRequest(model),
Err(err) => { Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse { return InternalEventPayload::ErrorResponse(ErrorResponse {
@@ -442,8 +455,7 @@ fn build_shared_reply(
} }
} }
"folder" => match query_manager "folder" => match query_manager
.connect() .with_tx(|tx| tx.delete_folder_by_id(&req.id, &UpdateSource::Plugin))
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)
{ {
Ok(model) => AnyModel::Folder(model), Ok(model) => AnyModel::Folder(model),
Err(err) => { Err(err) => {
@@ -454,8 +466,7 @@ fn build_shared_reply(
}, },
"environment" => { "environment" => {
match query_manager match query_manager
.connect() .with_tx(|tx| tx.delete_environment_by_id(&req.id, &UpdateSource::Plugin))
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)
{ {
Ok(model) => AnyModel::Environment(model), Ok(model) => AnyModel::Environment(model),
Err(err) => { Err(err) => {
@@ -508,20 +519,16 @@ mod tests {
yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB"); yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB");
query_manager query_manager
.connect() .with_tx(|tx| {
.upsert_workspace( tx.upsert_workspace(
&Workspace { &Workspace {
id: "wk_test".to_string(), id: "wk_test".to_string(),
name: "Workspace".to_string(), name: "Workspace".to_string(),
..Default::default() ..Default::default()
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )?;
.expect("Failed to seed workspace"); tx.upsert_folder(
query_manager
.connect()
.upsert_folder(
&Folder { &Folder {
id: "fl_test".to_string(), id: "fl_test".to_string(),
workspace_id: "wk_test".to_string(), workspace_id: "wk_test".to_string(),
@@ -529,12 +536,8 @@ mod tests {
..Default::default() ..Default::default()
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )?;
.expect("Failed to seed folder"); tx.upsert_http_request(
query_manager
.connect()
.upsert_http_request(
&HttpRequest { &HttpRequest {
id: "rq_test".to_string(), id: "rq_test".to_string(),
workspace_id: "wk_test".to_string(), workspace_id: "wk_test".to_string(),
@@ -546,7 +549,8 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
.expect("Failed to seed request"); })
.expect("Failed to seed");
(query_manager, temp_dir) (query_manager, temp_dir)
} }
+10 -7
View File
@@ -126,16 +126,17 @@ mod tests {
.unwrap(); .unwrap();
query_manager query_manager
.connect() .with_tx(|tx| {
.upsert_workspace( tx.upsert_workspace(
&Workspace { id: "wk_test".to_string(), ..Default::default() }, &Workspace { id: "wk_test".to_string(), ..Default::default() },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.unwrap(); .unwrap();
query_manager query_manager
.connect() .with_tx(|tx| {
.upsert_http_request( tx.upsert_http_request(
&HttpRequest { &HttpRequest {
id: "rq_test".to_string(), id: "rq_test".to_string(),
workspace_id: "wk_test".to_string(), workspace_id: "wk_test".to_string(),
@@ -143,6 +144,7 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.unwrap(); .unwrap();
let body_path = body.map(|bytes| { let body_path = body.map(|bytes| {
@@ -153,8 +155,8 @@ mod tests {
}); });
let response = query_manager let response = query_manager
.connect() .with_tx(|tx| {
.upsert_http_response( tx.upsert_http_response(
&HttpResponse { &HttpResponse {
workspace_id: "wk_test".to_string(), workspace_id: "wk_test".to_string(),
request_id: "rq_test".to_string(), request_id: "rq_test".to_string(),
@@ -168,6 +170,7 @@ mod tests {
&UpdateSource::Sync, &UpdateSource::Sync,
&blob_manager, &blob_manager,
) )
})
.unwrap(); .unwrap();
let id = response.id.clone(); let id = response.id.clone();
@@ -209,7 +212,7 @@ mod tests {
let mut response = qm.connect().get_http_response(&id).unwrap(); let mut response = qm.connect().get_http_response(&id).unwrap();
response.state = HttpResponseState::Closed; response.state = HttpResponseState::Closed;
qm.connect().update_http_response_if_id(&response, &UpdateSource::Sync).unwrap(); qm.with_tx(|tx| tx.update_http_response_if_id(&response, &UpdateSource::Sync)).unwrap();
assert!(FileResponseBodyStore::new(&qm).info(&id).unwrap().complete); assert!(FileResponseBodyStore::new(&qm).info(&id).unwrap().complete);
} }
+44 -36
View File
@@ -646,8 +646,9 @@ pub async fn send_http_request<T: TemplateCallback>(
if let Some(store) = store { if let Some(store) = store {
response = store response = store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response(&response, &store.update_source, store.blob_manager) tx.upsert_http_response(&response, &store.update_source, store.blob_manager)
})
.map_err(SendHttpRequestError::PersistResponse)?; .map_err(SendHttpRequestError::PersistResponse)?;
} else if response.id.is_empty() { } else if response.id.is_empty() {
response.id = generate_prefixed_id("rs"); response.id = generate_prefixed_id("rs");
@@ -700,8 +701,8 @@ pub async fn send_http_request<T: TemplateCallback>(
&event_workspace_id, &event_workspace_id,
event.clone().into(), event.clone().into(),
); );
if let Err(err) = if let Err(err) = query_manager
query_manager.connect().upsert_http_response_event(&db_event, update_source) .with_tx(|tx| tx.upsert_http_response_event(&db_event, update_source))
{ {
warn!("Failed to persist HTTP response event: {}", err); warn!("Failed to persist HTTP response event: {}", err);
} }
@@ -799,8 +800,13 @@ pub async fn send_http_request<T: TemplateCallback>(
if let Some(store) = store { if let Some(store) = store {
response = store response = store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response(&connected_response, &store.update_source, store.blob_manager) tx.upsert_http_response(
&connected_response,
&store.update_source,
store.blob_manager,
)
})
.map_err(SendHttpRequestError::PersistResponse)?; .map_err(SendHttpRequestError::PersistResponse)?;
} else { } else {
response = connected_response; response = connected_response;
@@ -886,12 +892,13 @@ pub async fn send_http_request<T: TemplateCallback>(
if let Some(store) = store { if let Some(store) = store {
response = store response = store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response( tx.upsert_http_response(
&progress_response, &progress_response,
&store.update_source, &store.update_source,
store.blob_manager, store.blob_manager,
) )
})
.map_err(SendHttpRequestError::PersistResponse)?; .map_err(SendHttpRequestError::PersistResponse)?;
} else { } else {
response = progress_response; response = progress_response;
@@ -960,8 +967,9 @@ pub async fn send_http_request<T: TemplateCallback>(
if let Some(store) = store { if let Some(store) = store {
response = store response = store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response(&final_response, &store.update_source, store.blob_manager) tx.upsert_http_response(&final_response, &store.update_source, store.blob_manager)
})
.map_err(SendHttpRequestError::PersistResponse)?; .map_err(SendHttpRequestError::PersistResponse)?;
} else { } else {
response = final_response; response = final_response;
@@ -998,8 +1006,9 @@ pub async fn send_http_request<T: TemplateCallback>(
if update_response && let Some(store) = store { if update_response && let Some(store) = store {
response = store response = store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response(&response, &store.update_source, store.blob_manager) tx.upsert_http_response(&response, &store.update_source, store.blob_manager)
})
.map_err(SendHttpRequestError::PersistResponse)?; .map_err(SendHttpRequestError::PersistResponse)?;
} }
} }
@@ -1027,17 +1036,14 @@ fn persist_request_body_bytes(
return Ok(()); return Ok(());
} }
let blob_ctx = blob_manager.connect(); blob_manager
let mut offset = 0; .with_tx(|b| {
let mut chunk_index: i32 = 0; for (chunk_index, data) in bytes.chunks(REQUEST_BODY_CHUNK_SIZE).enumerate() {
while offset < bytes.len() { b.insert_chunk(&BodyChunk::new(body_id, chunk_index as i32, data.to_vec()))?;
let end = std::cmp::min(offset + REQUEST_BODY_CHUNK_SIZE, bytes.len());
let chunk = BodyChunk::new(body_id, chunk_index, bytes[offset..end].to_vec());
blob_ctx.insert_chunk(&chunk).map_err(|e| e.to_string())?;
chunk_index += 1;
offset = end;
} }
Ok(()) Ok::<_, yaak_models::error::Error>(())
})
.map_err(|e| e.to_string())
} }
async fn persist_request_body_stream( async fn persist_request_body_stream(
@@ -1057,14 +1063,14 @@ async fn persist_request_body_stream(
while buf.len() >= REQUEST_BODY_CHUNK_SIZE { while buf.len() >= REQUEST_BODY_CHUNK_SIZE {
let data = buf.drain(..REQUEST_BODY_CHUNK_SIZE).collect(); let data = buf.drain(..REQUEST_BODY_CHUNK_SIZE).collect();
let chunk = BodyChunk::new(&body_id, chunk_index, data); let chunk = BodyChunk::new(&body_id, chunk_index, data);
blob_manager.connect().insert_chunk(&chunk).map_err(|e| e.to_string())?; blob_manager.with_tx(|b| b.insert_chunk(&chunk)).map_err(|e| e.to_string())?;
chunk_index += 1; chunk_index += 1;
} }
} }
if !buf.is_empty() { if !buf.is_empty() {
let chunk = BodyChunk::new(&body_id, chunk_index, buf); let chunk = BodyChunk::new(&body_id, chunk_index, buf);
blob_manager.connect().insert_chunk(&chunk).map_err(|e| e.to_string())?; blob_manager.with_tx(|b| b.insert_chunk(&chunk)).map_err(|e| e.to_string())?;
} }
Ok(total_bytes) Ok(total_bytes)
@@ -1114,8 +1120,7 @@ pub fn persist_cookies_after_send(
cookie_jar.cookies = cookies; cookie_jar.cookies = cookies;
query_manager query_manager
.connect() .with_tx(|tx| tx.upsert_cookie_jar(cookie_jar, &UpdateSource::Background))
.upsert_cookie_jar(cookie_jar, &UpdateSource::Background)
.map_err(SendHttpRequestError::PersistCookieJar)?; .map_err(SendHttpRequestError::PersistCookieJar)?;
Ok(()) Ok(())
} }
@@ -1209,8 +1214,8 @@ fn persist_response_error(
let elapsed = duration_to_i32(started_at.elapsed()); let elapsed = duration_to_i32(started_at.elapsed());
store store
.query_manager .query_manager
.connect() .with_tx(|tx| {
.upsert_http_response( tx.upsert_http_response(
&HttpResponse { &HttpResponse {
state: HttpResponseState::Closed, state: HttpResponseState::Closed,
elapsed, elapsed,
@@ -1226,6 +1231,7 @@ fn persist_response_error(
&store.update_source, &store.update_source,
store.blob_manager, store.blob_manager,
) )
})
.map_err(SendHttpRequestError::PersistResponse) .map_err(SendHttpRequestError::PersistResponse)
} }
@@ -1444,15 +1450,16 @@ mod tests {
.expect("Failed to initialize DB"); .expect("Failed to initialize DB");
query_manager query_manager
.connect() .with_tx(|tx| {
.upsert_workspace( tx.upsert_workspace(
&Workspace { id: "wk_test".to_string(), ..Default::default() }, &Workspace { id: "wk_test".to_string(), ..Default::default() },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to seed workspace"); .expect("Failed to seed workspace");
let cookie_jar = query_manager let cookie_jar = query_manager
.connect() .with_tx(|tx| {
.upsert_cookie_jar( tx.upsert_cookie_jar(
&CookieJar { &CookieJar {
id: "cj_test".to_string(), id: "cj_test".to_string(),
workspace_id: "wk_test".to_string(), workspace_id: "wk_test".to_string(),
@@ -1461,6 +1468,7 @@ mod tests {
}, },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to seed cookie jar"); .expect("Failed to seed cookie jar");
(query_manager, cookie_jar, temp_dir) (query_manager, cookie_jar, temp_dir)
@@ -1507,18 +1515,18 @@ mod tests {
let (query_manager, mut cookie_jar, _temp_dir) = seed_cookie_jar(); let (query_manager, mut cookie_jar, _temp_dir) = seed_cookie_jar();
cookie_jar.cookies = vec![cookie("original")]; cookie_jar.cookies = vec![cookie("original")];
cookie_jar = query_manager cookie_jar = query_manager
.connect() .with_tx(|tx| tx.upsert_cookie_jar(&cookie_jar, &UpdateSource::Sync))
.upsert_cookie_jar(&cookie_jar, &UpdateSource::Sync)
.expect("Failed to seed cookies"); .expect("Failed to seed cookies");
let store = CookieStore::from_cookies(cookie_jar.cookies.clone()); let store = CookieStore::from_cookies(cookie_jar.cookies.clone());
// Someone else updates the jar while the send is in flight. // Someone else updates the jar while the send is in flight.
query_manager query_manager
.connect() .with_tx(|tx| {
.upsert_cookie_jar( tx.upsert_cookie_jar(
&CookieJar { cookies: vec![cookie("newer")], ..cookie_jar.clone() }, &CookieJar { cookies: vec![cookie("newer")], ..cookie_jar.clone() },
&UpdateSource::Sync, &UpdateSource::Sync,
) )
})
.expect("Failed to update cookie jar"); .expect("Failed to update cookie jar");
persist_cookies_after_send(&query_manager, Some(&mut cookie_jar), Some(&store)) persist_cookies_after_send(&query_manager, Some(&mut cookie_jar), Some(&store))