mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-14 13:52:01 +02:00
Route all database writes through one connection (#642)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ff7eebf3cd
commit
90cb578e26
@@ -49,8 +49,11 @@ fn schema(pretty: bool) -> CommandResult {
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?;
|
||||
let environments = ctx
|
||||
.db()
|
||||
.list_environments_ensure_base(&workspace_id)
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.ensure_base_environment(&workspace_id)?;
|
||||
tx.list_environments(&workspace_id)
|
||||
})
|
||||
.map_err(|e| format!("Failed to list environments: {e}"))?;
|
||||
|
||||
if environments.is_empty() {
|
||||
@@ -111,8 +114,8 @@ fn create(
|
||||
}
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_environment(&environment, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_environment(&environment, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create environment: {e}"))?;
|
||||
|
||||
println!("Created environment: {}", created.id);
|
||||
@@ -133,8 +136,8 @@ fn create(
|
||||
};
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_environment(&environment, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_environment(&environment, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create environment: {e}"))?;
|
||||
|
||||
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 saved = ctx
|
||||
.db()
|
||||
.upsert_environment(&updated, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_environment(&updated, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to update environment: {e}"))?;
|
||||
|
||||
println!("Updated environment: {}", saved.id);
|
||||
@@ -167,8 +170,8 @@ fn delete(ctx: &CliContext, environment_id: &str, yes: bool) -> CommandResult {
|
||||
}
|
||||
|
||||
let deleted = ctx
|
||||
.db()
|
||||
.delete_environment_by_id(environment_id, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.delete_environment_by_id(environment_id, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to delete environment: {e}"))?;
|
||||
|
||||
println!("Deleted environment: {}", deleted.id);
|
||||
|
||||
@@ -102,8 +102,8 @@ fn create(
|
||||
)?;
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_folder(&folder, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create folder: {e}"))?;
|
||||
|
||||
println!("Created folder: {}", created.id);
|
||||
@@ -118,8 +118,8 @@ fn create(
|
||||
let folder = Folder { workspace_id, name, ..Default::default() };
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_folder(&folder, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create folder: {e}"))?;
|
||||
|
||||
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 saved = ctx
|
||||
.db()
|
||||
.upsert_folder(&updated, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_folder(&updated, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to update folder: {e}"))?;
|
||||
|
||||
println!("Updated folder: {}", saved.id);
|
||||
@@ -150,8 +150,8 @@ fn delete(ctx: &CliContext, folder_id: &str, yes: bool) -> CommandResult {
|
||||
}
|
||||
|
||||
let deleted = ctx
|
||||
.db()
|
||||
.delete_folder_by_id(folder_id, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.delete_folder_by_id(folder_id, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to delete folder: {e}"))?;
|
||||
|
||||
println!("Deleted folder: {}", deleted.id);
|
||||
|
||||
@@ -348,8 +348,9 @@ async fn install_from_directory(context: &CliContext, source: &str) -> CommandRe
|
||||
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
|
||||
|
||||
let plugin = context
|
||||
.db()
|
||||
.upsert_plugin(
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_plugin(
|
||||
&Plugin {
|
||||
directory: plugin_dir_str,
|
||||
url: None,
|
||||
@@ -359,6 +360,7 @@ async fn install_from_directory(context: &CliContext, source: &str) -> CommandRe
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
})
|
||||
.map_err(|err| format!("Failed to save plugin in database: {err}"))?;
|
||||
|
||||
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
||||
|
||||
@@ -424,8 +424,8 @@ fn create(
|
||||
)?;
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_http_request(&request, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create request: {e}"))?;
|
||||
|
||||
println!("Created request: {}", created.id);
|
||||
@@ -443,8 +443,8 @@ fn create(
|
||||
}
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_http_request(&request, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create request: {e}"))?;
|
||||
|
||||
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 saved = ctx
|
||||
.db()
|
||||
.upsert_http_request(&updated, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_http_request(&updated, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to update request: {e}"))?;
|
||||
|
||||
println!("Updated request: {}", saved.id);
|
||||
@@ -487,8 +487,8 @@ fn delete(ctx: &CliContext, request_id: &str, yes: bool) -> CommandResult {
|
||||
}
|
||||
|
||||
let deleted = ctx
|
||||
.db()
|
||||
.delete_http_request_by_id(request_id, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.delete_http_request_by_id(request_id, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to delete request: {e}"))?;
|
||||
println!("Deleted request: {}", deleted.id);
|
||||
Ok(())
|
||||
|
||||
@@ -118,8 +118,8 @@ fn delete(ctx: &CliContext, id: &str, yes: bool) -> CommandResult {
|
||||
return Ok(());
|
||||
}
|
||||
let count = ctx
|
||||
.db()
|
||||
.delete_all_http_responses_for_request(id, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.delete_all_http_responses_for_request(id, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to delete responses: {e}"))?;
|
||||
println!("Deleted {count} responses for request {id}");
|
||||
return Ok(());
|
||||
@@ -131,8 +131,10 @@ fn delete(ctx: &CliContext, id: &str, yes: bool) -> CommandResult {
|
||||
return Ok(());
|
||||
}
|
||||
let count = ctx
|
||||
.db()
|
||||
.delete_all_http_responses_for_workspace(&workspace_id, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.delete_all_http_responses_for_workspace(&workspace_id, &UpdateSource::Sync)
|
||||
})
|
||||
.map_err(|e| format!("Failed to delete responses: {e}"))?;
|
||||
println!("Deleted {count} responses for workspace {workspace_id}");
|
||||
Ok(())
|
||||
|
||||
@@ -84,8 +84,8 @@ fn create(
|
||||
.map_err(|e| format!("Failed to parse workspace create JSON: {e}"))?;
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_workspace(&workspace, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create workspace: {e}"))?;
|
||||
println!("Created workspace: {}", created.id);
|
||||
return Ok(());
|
||||
@@ -97,8 +97,8 @@ fn create(
|
||||
|
||||
let workspace = Workspace { name, ..Default::default() };
|
||||
let created = ctx
|
||||
.db()
|
||||
.upsert_workspace(&workspace, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to create workspace: {e}"))?;
|
||||
println!("Created workspace: {}", created.id);
|
||||
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 saved = ctx
|
||||
.db()
|
||||
.upsert_workspace(&updated, &UpdateSource::Sync)
|
||||
.query_manager()
|
||||
.with_tx(|tx| tx.upsert_workspace(&updated, &UpdateSource::Sync))
|
||||
.map_err(|e| format!("Failed to update workspace: {e}"))?;
|
||||
|
||||
println!("Updated workspace: {}", saved.id);
|
||||
@@ -130,8 +130,10 @@ fn delete(ctx: &CliContext, workspace_id: &str, yes: bool) -> CommandResult {
|
||||
}
|
||||
|
||||
let deleted = ctx
|
||||
.db()
|
||||
.delete_workspace_by_id(workspace_id, &UpdateSource::Sync, ctx.blob_manager())
|
||||
.query_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}"))?;
|
||||
println!("Deleted workspace: {}", deleted.id);
|
||||
Ok(())
|
||||
|
||||
@@ -51,11 +51,9 @@ impl CliContext {
|
||||
};
|
||||
|
||||
// Guest: the desktop may have this DB open, so only what's safe beside a live session
|
||||
let _ = yaak_lifecycle::on_launch(
|
||||
&yaak_lifecycle::Host::guest(),
|
||||
&query_manager.connect(),
|
||||
&blob_manager,
|
||||
);
|
||||
let _ = query_manager.with_tx(|tx| {
|
||||
yaak_lifecycle::on_launch(&yaak_lifecycle::Host::guest(), tx, &blob_manager)
|
||||
});
|
||||
|
||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ pub fn seed_workspace(data_dir: &Path, workspace_id: &str) {
|
||||
};
|
||||
|
||||
query_manager(data_dir)
|
||||
.connect()
|
||||
.upsert_workspace(&workspace, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_workspace(&workspace, &UpdateSource::Sync))
|
||||
.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)
|
||||
.connect()
|
||||
.upsert_http_request(&request, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
|
||||
.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)
|
||||
.connect()
|
||||
.upsert_folder(&folder, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_folder(&folder, &UpdateSource::Sync))
|
||||
.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)
|
||||
.connect()
|
||||
.upsert_grpc_request(&request, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_grpc_request(&request, &UpdateSource::Sync))
|
||||
.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)
|
||||
.connect()
|
||||
.upsert_websocket_request(&request, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_websocket_request(&request, &UpdateSource::Sync))
|
||||
.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 db = query_manager.connect();
|
||||
let environments =
|
||||
db.list_environments_ensure_base(&workspace_id).expect("list imported environments");
|
||||
let environments = db.list_environments(&workspace_id).expect("list imported environments");
|
||||
|
||||
let imported_environment =
|
||||
environments.iter().find(|e| e.name == "Local").expect("postman environment imported");
|
||||
@@ -299,7 +298,10 @@ fn re_import_leaves_deleted_resources_alone() {
|
||||
.into_iter()
|
||||
.find(|r| r.name == "Request B")
|
||||
.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
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
||||
..Default::default()
|
||||
};
|
||||
query_manager(data_dir)
|
||||
.connect()
|
||||
.upsert_http_request(&request, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_http_request(&request, &UpdateSource::Sync))
|
||||
.expect("Failed to seed folder request");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
|
||||
@@ -330,7 +330,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
let settings = app_handle.db().get_settings();
|
||||
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 {
|
||||
workspace_id: request.workspace_id.clone(),
|
||||
request_id: request.id.clone(),
|
||||
@@ -341,7 +342,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
let conn_id = conn.id.clone();
|
||||
|
||||
@@ -386,7 +388,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
let connection = match connection {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
app_handle.db().upsert_grpc_connection(
|
||||
app_handle.with_tx(|tx| {
|
||||
tx.upsert_grpc_connection(
|
||||
&GrpcConnection {
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
error: Some(err.to_string()),
|
||||
@@ -394,7 +397,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
..conn.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
return Ok(conn_id);
|
||||
}
|
||||
};
|
||||
@@ -495,7 +499,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
.await?;
|
||||
let msg = strip_json_comments(&msg);
|
||||
|
||||
app_handle.db().upsert_grpc_event(
|
||||
app_handle.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: format!("Connecting to {}", req.url),
|
||||
event_type: GrpcEventType::ConnectionStart,
|
||||
@@ -503,7 +508,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
async move {
|
||||
// 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();
|
||||
move |result: std::result::Result<String, String>| match result {
|
||||
Ok(msg) => {
|
||||
let _ = app_handle.db().upsert_grpc_event(
|
||||
let _ = app_handle.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: msg,
|
||||
event_type: GrpcEventType::ClientMessage,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(&window_label),
|
||||
);
|
||||
)
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = app_handle.db().upsert_grpc_event(
|
||||
let _ = app_handle.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: format!("Failed to send message: {}", error),
|
||||
event_type: GrpcEventType::Error,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(&window_label),
|
||||
);
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -583,8 +593,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
|
||||
if !method_desc.is_client_streaming() {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
event_type: GrpcEventType::ClientMessage,
|
||||
content: msg,
|
||||
@@ -592,14 +602,15 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
match maybe_msg {
|
||||
Some(Ok(msg)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
metadata: metadata_to_map(msg.metadata().clone()),
|
||||
content: if msg.metadata().len() == 0 {
|
||||
@@ -613,6 +624,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let response_message = msg.into_inner();
|
||||
let content = match connection
|
||||
@@ -622,8 +634,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Failed to read response".to_string(),
|
||||
error: Some(err.to_string()),
|
||||
@@ -633,13 +645,14 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content,
|
||||
event_type: GrpcEventType::ServerMessage,
|
||||
@@ -647,10 +660,11 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Connection complete".to_string(),
|
||||
event_type: GrpcEventType::ConnectionEnd,
|
||||
@@ -659,12 +673,13 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&(match e.status {
|
||||
Some(s) => GrpcEvent {
|
||||
error: Some(s.message().to_string()),
|
||||
@@ -684,12 +699,13 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
}),
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
error: Some(e.to_string()),
|
||||
status: Some(Code::Unknown as i32),
|
||||
@@ -699,6 +715,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
@@ -709,8 +726,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
let mut stream = match maybe_stream {
|
||||
Some(Ok(stream)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
metadata: metadata_to_map(stream.metadata().clone()),
|
||||
content: if stream.metadata().len() == 0 {
|
||||
@@ -724,14 +741,15 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
stream.into_inner()
|
||||
}
|
||||
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
|
||||
warn!("GRPC stream error {e:?}");
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&(match e.status {
|
||||
Some(s) => GrpcEvent {
|
||||
error: Some(s.message().to_string()),
|
||||
@@ -751,13 +769,14 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
}),
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
error: Some(e.to_string()),
|
||||
status: Some(Code::Unknown as i32),
|
||||
@@ -767,6 +786,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
@@ -783,8 +803,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Failed to read response".to_string(),
|
||||
error: Some(err.to_string()),
|
||||
@@ -794,13 +814,14 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
};
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: message,
|
||||
event_type: GrpcEventType::ServerMessage,
|
||||
@@ -808,14 +829,15 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Ok(None) => {
|
||||
let trailers =
|
||||
stream.trailers().await.unwrap_or_default().unwrap_or_default();
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Connection complete".to_string(),
|
||||
status: Some(Code::Ok as i32),
|
||||
@@ -825,13 +847,14 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
Err(status) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Stream failed".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()),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
@@ -874,7 +898,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
}).unwrap();
|
||||
},
|
||||
_ = cancelled_rx.changed() => {
|
||||
w.db().upsert_grpc_event(
|
||||
w.with_tx(|tx| {
|
||||
tx.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: "Cancelled".to_string(),
|
||||
event_type: GrpcEventType::ConnectionEnd,
|
||||
@@ -882,7 +907,9 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
..base_msg.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
).unwrap();
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
w.with_tx(|c| {
|
||||
c.upsert_grpc_connection(
|
||||
&GrpcConnection{
|
||||
@@ -1066,7 +1093,8 @@ async fn cmd_send_http_request<R: Runtime>(
|
||||
let request = app_handle.db().get_http_request(&request_id)?;
|
||||
|
||||
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 {
|
||||
request_id: request.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()),
|
||||
&blobs,
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
|
||||
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,
|
||||
Err(e) => {
|
||||
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 {
|
||||
state: HttpResponseState::Closed,
|
||||
error: Some(e.to_string()),
|
||||
@@ -1120,7 +1150,8 @@ async fn cmd_send_http_request<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
&blobs,
|
||||
)?
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1254,8 +1285,8 @@ pub fn run() {
|
||||
.setup(|app| {
|
||||
let lifecycle_host = yaak_lifecycle::Host::owner()
|
||||
.with_responses_dir(app.path().app_data_dir()?.join("responses"));
|
||||
if let Err(e) =
|
||||
yaak_lifecycle::on_launch(&lifecycle_host, &app.db(), &app.blob_manager())
|
||||
if let Err(e) = app
|
||||
.with_tx(|tx| yaak_lifecycle::on_launch(&lifecycle_host, tx, &app.blob_manager()))
|
||||
{
|
||||
error!("on_launch hook failed: {e:?}");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Emitter, Manager, Runtime, State};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
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::query_manager::QueryManager;
|
||||
use yaak_models::util::{ModelPayload, UpdateSource};
|
||||
@@ -95,7 +95,7 @@ pub trait QueryManagerExt<'a, R> {
|
||||
fn db(&'a self) -> ClientDb<'a>;
|
||||
fn with_tx<F, T>(&'a self, func: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(&ClientDb) -> Result<T>;
|
||||
F: FnOnce(&WriteDb) -> Result<T>;
|
||||
}
|
||||
|
||||
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>
|
||||
where
|
||||
F: FnOnce(&ClientDb) -> Result<T>,
|
||||
F: FnOnce(&WriteDb) -> Result<T>,
|
||||
{
|
||||
let qm = self.state::<QueryManager>();
|
||||
qm.inner().with_tx(func)
|
||||
|
||||
@@ -55,13 +55,16 @@ impl YaakNotifier {
|
||||
seen.push(id.to_string());
|
||||
debug!("Marked notification as seen {}", id);
|
||||
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_KEY,
|
||||
seen_json.as_str(),
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 };
|
||||
app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin)?;
|
||||
app_handle.with_tx(|tx| tx.upsert_plugin(&new_plugin, &UpdateSource::Plugin))?;
|
||||
}
|
||||
|
||||
if !req.silent {
|
||||
@@ -294,7 +294,8 @@ async fn handle_host_plugin_request<R: Runtime>(
|
||||
HttpResponse::default()
|
||||
} else {
|
||||
let blobs = window.blob_manager();
|
||||
window.db().upsert_http_response(
|
||||
window.with_tx(|tx| {
|
||||
tx.upsert_http_response(
|
||||
&HttpResponse {
|
||||
request_id: http_request.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()),
|
||||
&blobs,
|
||||
)?
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
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
|
||||
// can't include it and boot it a second time
|
||||
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 {
|
||||
directory: directory.into(),
|
||||
url: None,
|
||||
@@ -211,7 +212,8 @@ pub async fn cmd_plugins_install_from_directory<R: Runtime>(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?;
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ use tokio::sync::watch;
|
||||
use yaak_rpc_schema::WatchResult;
|
||||
use yaak_sync::error::Error::InvalidSyncDirectory;
|
||||
use yaak_sync::sync::{
|
||||
FsCandidate, SyncOp, apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates,
|
||||
get_fs_candidates,
|
||||
FsCandidate, SyncOp, apply_db_sync_ops, apply_fs_sync_ops, apply_sync_state_ops,
|
||||
compute_sync_ops, get_db_candidates, get_fs_candidates,
|
||||
};
|
||||
use yaak_sync::watch::{WatchEvent, watch_directory};
|
||||
|
||||
@@ -49,11 +49,14 @@ pub(crate) async fn cmd_sync_apply<R: Runtime>(
|
||||
sync_dir: &Path,
|
||||
workspace_id: &str,
|
||||
) -> 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 sync_state_ops = apply_sync_ops(&db, &blobs, workspace_id, sync_dir, sync_ops)?;
|
||||
apply_sync_state_ops(&db, workspace_id, sync_dir, sync_state_ops)?;
|
||||
app_handle.db_manager().with_tx(|tx| {
|
||||
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(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn sync_watch<R, F>(
|
||||
|
||||
@@ -43,7 +43,8 @@ pub async fn cmd_ws_send<R: Runtime>(
|
||||
{
|
||||
Ok(connection) => Ok(connection),
|
||||
Err(e) => {
|
||||
app_handle.db().upsert_websocket_event(
|
||||
app_handle.with_tx(|tx| {
|
||||
tx.upsert_websocket_event(
|
||||
&WebsocketEvent {
|
||||
connection_id: connection.id.clone(),
|
||||
request_id: connection.request_id.clone(),
|
||||
@@ -54,7 +55,8 @@ pub async fn cmd_ws_send<R: Runtime>(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(connection)
|
||||
}
|
||||
@@ -96,7 +98,8 @@ async fn send_websocket_message<R: Runtime>(
|
||||
let mut ws_manager = ws_manager.lock().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 {
|
||||
connection_id: connection.id.clone(),
|
||||
request_id: request.id.clone(),
|
||||
@@ -107,7 +110,8 @@ async fn send_websocket_message<R: Runtime>(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(connection.clone())
|
||||
}
|
||||
@@ -118,14 +122,13 @@ pub async fn cmd_ws_close<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
||||
) -> Result<WebsocketConnection> {
|
||||
let connection = {
|
||||
let db = app_handle.db();
|
||||
let connection = db.get_websocket_connection(connection_id)?;
|
||||
db.upsert_websocket_connection(
|
||||
let connection = app_handle.with_tx(|tx| {
|
||||
let connection = tx.get_websocket_connection(connection_id)?;
|
||||
tx.upsert_websocket_connection(
|
||||
&WebsocketConnection { state: WebsocketConnectionState::Closing, ..connection },
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?
|
||||
};
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut ws_manager = ws_manager.lock().await;
|
||||
if let Err(e) = ws_manager.close(&connection.id).await {
|
||||
@@ -169,14 +172,16 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let connection = app_handle.db().upsert_websocket_connection(
|
||||
let connection = app_handle.with_tx(|tx| {
|
||||
tx.upsert_websocket_connection(
|
||||
&WebsocketConnection {
|
||||
workspace_id: request.workspace_id.clone(),
|
||||
request_id: request_id.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
let (mut url, url_parameters) = apply_path_placeholders(&request.url, &request.url_parameters);
|
||||
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) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
return Ok(app_handle.db().upsert_websocket_connection(
|
||||
return Ok(app_handle.with_tx(|tx| {
|
||||
tx.upsert_websocket_connection(
|
||||
&WebsocketConnection {
|
||||
error: Some(format!("Failed to parse URL {}", e.to_string())),
|
||||
state: WebsocketConnectionState::Closed,
|
||||
..connection
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?);
|
||||
)
|
||||
})?);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -321,18 +328,21 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(app_handle.db().upsert_websocket_connection(
|
||||
return Ok(app_handle.with_tx(|tx| {
|
||||
tx.upsert_websocket_connection(
|
||||
&WebsocketConnection {
|
||||
error: Some(e.to_string()),
|
||||
state: WebsocketConnectionState::Closed,
|
||||
..connection
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?);
|
||||
)
|
||||
})?);
|
||||
}
|
||||
};
|
||||
|
||||
app_handle.db().upsert_websocket_event(
|
||||
app_handle.with_tx(|tx| {
|
||||
tx.upsert_websocket_event(
|
||||
&WebsocketEvent {
|
||||
connection_id: connection.id.clone(),
|
||||
request_id: request.id.clone(),
|
||||
@@ -342,7 +352,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
let response_headers = response
|
||||
.headers()
|
||||
@@ -366,11 +377,12 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
if !set_cookie_headers.is_empty() {
|
||||
store.store_cookies_from_response(&convert_ws_url_to_http(&url), &set_cookie_headers);
|
||||
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 {
|
||||
state: WebsocketConnectionState::Connected,
|
||||
headers: response_headers,
|
||||
@@ -379,7 +391,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
..connection
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
{
|
||||
let connection_id = connection.id.clone();
|
||||
@@ -395,8 +408,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
}
|
||||
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_websocket_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_websocket_event(
|
||||
&WebsocketEvent {
|
||||
connection_id: connection_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),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
info!("Websocket connection closed");
|
||||
if !has_written_close {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_websocket_event(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_websocket_event(
|
||||
&WebsocketEvent {
|
||||
connection_id: connection_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),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_websocket_connection(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_websocket_connection(
|
||||
&WebsocketConnection {
|
||||
workspace_id: request.workspace_id.clone(),
|
||||
request_id: request_id.to_string(),
|
||||
@@ -446,6 +461,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
},
|
||||
&UpdateSource::from_window_label(&window_label),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||
use ts_rs::TS;
|
||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
||||
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::util::UpdateSource;
|
||||
|
||||
@@ -17,6 +17,10 @@ use yaak_models::util::UpdateSource;
|
||||
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
||||
trait QueryManagerExt<'a, R> {
|
||||
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 {
|
||||
@@ -24,6 +28,14 @@ impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
|
||||
let qm = self.state::<QueryManager>();
|
||||
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";
|
||||
@@ -137,12 +149,17 @@ pub async fn activate_license<R: Runtime>(
|
||||
}
|
||||
|
||||
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_NAMESPACE,
|
||||
body.activation_id.as_str(),
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
);
|
||||
Ok(())
|
||||
}) {
|
||||
warn!("Failed to store license activation: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = window.emit("license-activated", true) {
|
||||
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);
|
||||
}
|
||||
|
||||
app_handle.db().delete_key_value(
|
||||
app_handle.with_tx(|tx| {
|
||||
tx.delete_key_value(
|
||||
KV_ACTIVATION_ID_KEY,
|
||||
KV_NAMESPACE,
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Err(e) = app_handle.emit("license-deactivated", true) {
|
||||
warn!("Failed to emit deactivate-license event: {}", e);
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
//! 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
|
||||
//! 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"))]
|
||||
mod imp {
|
||||
|
||||
@@ -72,7 +72,7 @@ pub trait Host: Clone {
|
||||
self.query_manager().connect()
|
||||
}
|
||||
|
||||
fn blobs(&self) -> BlobContext {
|
||||
fn blobs(&self) -> BlobContext<'_> {
|
||||
self.blob_manager().connect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ use yaak_models::queries::workspaces::default_headers;
|
||||
use yaak_rpc_schema::*;
|
||||
|
||||
pub async fn models_upsert<H: Host>(host: H, req: ModelsUpsertReq) -> Result<String> {
|
||||
let db = host.db();
|
||||
let blobs = host.blob_manager();
|
||||
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
|
||||
@@ -75,12 +76,9 @@ pub async fn models_upsert_graphql_introspection<H: Host>(
|
||||
req: ModelsUpsertGraphqlIntrospectionReq,
|
||||
) -> Result<GraphQlIntrospection> {
|
||||
let source = host.update_source();
|
||||
Ok(host.db().upsert_graphql_introspection(
|
||||
&req.workspace_id,
|
||||
&req.request_id,
|
||||
req.content,
|
||||
&source,
|
||||
)?)
|
||||
Ok(host.query_manager().with_tx(|tx| {
|
||||
tx.upsert_graphql_introspection(&req.workspace_id, &req.request_id, req.content, &source)
|
||||
})?)
|
||||
}
|
||||
|
||||
/// 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
|
||||
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();
|
||||
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_grpc_connections(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,
|
||||
req: CmdGetWorkspaceMetaReq,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
let db = host.db();
|
||||
let workspace = db.get_workspace(&req.workspace_id)?;
|
||||
Ok(db.get_or_create_workspace_meta(&workspace.id)?)
|
||||
let workspace = host.db().get_workspace(&req.workspace_id)?;
|
||||
Ok(host.query_manager().with_tx(|tx| tx.ensure_workspace_meta(&workspace.id))?)
|
||||
}
|
||||
|
||||
pub async fn cmd_delete_all_grpc_connections<H: Host>(
|
||||
host: H,
|
||||
req: CmdDeleteAllGrpcConnectionsReq,
|
||||
) -> 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>(
|
||||
host: H,
|
||||
req: CmdDeleteAllHttpResponsesReq,
|
||||
) -> 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(())
|
||||
}
|
||||
|
||||
@@ -156,9 +164,9 @@ pub async fn cmd_ws_delete_connections<H: Host>(
|
||||
host: H,
|
||||
req: CmdWsDeleteConnectionsReq,
|
||||
) -> Result<()> {
|
||||
Ok(host
|
||||
.db()
|
||||
.delete_all_websocket_connections_for_request(&req.request_id, &host.update_source())?)
|
||||
Ok(host.query_manager().with_tx(|tx| {
|
||||
tx.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<()> {
|
||||
|
||||
@@ -118,6 +118,7 @@ impl Host for TestHost {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn writes_carry_the_client_id() {
|
||||
let host = TestHost::new();
|
||||
host.drain_writes(); // the rows startup creates
|
||||
|
||||
let workspace = Workspace { name: "From a test".to_string(), ..Default::default() };
|
||||
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
|
||||
// variable is what proves the chain was resolved rather than skipped.
|
||||
let environment = host
|
||||
.db()
|
||||
.upsert_environment(
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: id.clone(),
|
||||
name: "Test env".to_string(),
|
||||
@@ -421,6 +423,7 @@ async fn a_single_threaded_host_can_implement_the_trait() {
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
})
|
||||
.expect("seed environment");
|
||||
|
||||
let rendered = cmd_render_template(
|
||||
@@ -459,14 +462,17 @@ async fn auth_values_are_rendered_before_the_host_sees_them() {
|
||||
};
|
||||
|
||||
let workspace = host
|
||||
.db()
|
||||
.upsert_workspace(
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_workspace(
|
||||
&Workspace { name: "Auth".to_string(), ..Default::default() },
|
||||
&host.update_source(),
|
||||
)
|
||||
})
|
||||
.expect("workspace");
|
||||
host.db()
|
||||
.upsert_environment(
|
||||
host.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: workspace.id.clone(),
|
||||
name: "Env".to_string(),
|
||||
@@ -480,9 +486,9 @@ async fn auth_values_are_rendered_before_the_host_sees_them() {
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
})
|
||||
.expect("environment");
|
||||
let environment =
|
||||
host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0);
|
||||
let environment = host.db().list_environments(&workspace.id).expect("list").remove(0);
|
||||
|
||||
let mut values = HashMap::new();
|
||||
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
|
||||
.db()
|
||||
.upsert_workspace(
|
||||
.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_workspace(
|
||||
&Workspace { name: "Functions".to_string(), ..Default::default() },
|
||||
&host.update_source(),
|
||||
)
|
||||
})
|
||||
.expect("workspace");
|
||||
host.db()
|
||||
.upsert_environment(
|
||||
host.query_manager()
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: workspace.id.clone(),
|
||||
name: "Env".to_string(),
|
||||
@@ -542,9 +551,9 @@ async fn template_function_values_are_rendered_before_the_host_sees_them() {
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
})
|
||||
.expect("environment");
|
||||
let environment =
|
||||
host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0);
|
||||
let environment = host.db().list_environments(&workspace.id).expect("list").remove(0);
|
||||
|
||||
let mut values = HashMap::new();
|
||||
values.insert("token".to_string(), JsonPrimitive::String("${[1PASSWORD_TOKEN]}".to_string()));
|
||||
|
||||
@@ -83,7 +83,7 @@ impl EncryptionManager {
|
||||
|
||||
let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| {
|
||||
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(
|
||||
&Workspace { encryption_key_challenge, ..workspace },
|
||||
&UpdateSource::Background,
|
||||
@@ -103,7 +103,7 @@ impl EncryptionManager {
|
||||
|
||||
pub fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
|
||||
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
|
||||
if let Some(_) = workspace_meta.encryption_key {
|
||||
@@ -120,7 +120,7 @@ impl EncryptionManager {
|
||||
|
||||
self.query_manager.with_tx::<(), Error>(|tx| {
|
||||
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
|
||||
tx.upsert_workspace(
|
||||
@@ -152,10 +152,12 @@ impl EncryptionManager {
|
||||
}
|
||||
};
|
||||
|
||||
let db = self.query_manager.connect();
|
||||
let workspace_meta = db.get_or_create_workspace_meta(workspace_id)?;
|
||||
|
||||
let key = match workspace_meta.encryption_key {
|
||||
let key = match self
|
||||
.query_manager
|
||||
.connect()
|
||||
.get_workspace_meta(workspace_id)
|
||||
.and_then(|m| m.encryption_key)
|
||||
{
|
||||
None => return Err(MissingWorkspaceKey),
|
||||
Some(k) => k,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use log::info;
|
||||
use std::path::PathBuf;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::client_db::WriteDb;
|
||||
use yaak_models::error::Result;
|
||||
|
||||
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.
|
||||
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)?;
|
||||
|
||||
if host.role == Role::Owner {
|
||||
@@ -77,23 +78,19 @@ mod tests {
|
||||
#[test]
|
||||
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 db = query_manager.connect();
|
||||
let source = &UpdateSource::Background;
|
||||
|
||||
let workspace = db
|
||||
.upsert_workspace(
|
||||
let pending = query_manager
|
||||
.with_tx(|db| {
|
||||
let workspace = db.upsert_workspace(
|
||||
&Workspace { name: "Hooks".to_string(), ..Default::default() },
|
||||
source,
|
||||
)
|
||||
.unwrap();
|
||||
let request = db
|
||||
.upsert_http_request(
|
||||
)?;
|
||||
let request = db.upsert_http_request(
|
||||
&HttpRequest { workspace_id: workspace.id.clone(), ..Default::default() },
|
||||
source,
|
||||
)
|
||||
.unwrap();
|
||||
let pending = db
|
||||
.upsert_http_response(
|
||||
)?;
|
||||
db.upsert_http_response(
|
||||
&HttpResponse {
|
||||
request_id: request.id.clone(),
|
||||
workspace_id: workspace.id.clone(),
|
||||
@@ -103,27 +100,26 @@ mod tests {
|
||||
source,
|
||||
&blob_manager,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
on_launch(&Host::guest(), &db, &blob_manager).unwrap();
|
||||
let response = db.get_http_response(&pending.id).unwrap();
|
||||
query_manager.with_tx(|db| on_launch(&Host::guest(), db, &blob_manager)).unwrap();
|
||||
let response = query_manager.connect().get_http_response(&pending.id).unwrap();
|
||||
assert!(matches!(response.state, HttpResponseState::Connected));
|
||||
|
||||
on_launch(&Host::owner(), &db, &blob_manager).unwrap();
|
||||
let response = db.get_http_response(&pending.id).unwrap();
|
||||
query_manager.with_tx(|db| on_launch(&Host::owner(), db, &blob_manager)).unwrap();
|
||||
let response = query_manager.connect().get_http_response(&pending.id).unwrap();
|
||||
assert!(matches!(response.state, HttpResponseState::Closed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_without_a_filesystem_still_sweeps_blobs() {
|
||||
let (query_manager, blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
|
||||
let db = query_manager.connect();
|
||||
{
|
||||
let blob_ctx = blob_manager.connect();
|
||||
blob_ctx.insert_chunk(&BodyChunk::new("rs_gone", 0, b"dead".to_vec())).unwrap();
|
||||
}
|
||||
blob_manager
|
||||
.with_tx(|b| b.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());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::util::generate_prefixed_id;
|
||||
use include_dir::{Dir, include_dir};
|
||||
use log::{debug, info};
|
||||
use rusqlite::{OptionalExtension, params};
|
||||
use yaak_database::{SqliteConn, SqlitePool};
|
||||
use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
|
||||
use std::ops::Deref;
|
||||
use yaak_database::{ConnectionOrTx, SqlitePool};
|
||||
|
||||
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations");
|
||||
|
||||
@@ -22,41 +24,64 @@ impl BodyChunk {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages the blob database connection pool.
|
||||
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
|
||||
/// Manages the blob database: a reader pool and a single writer, for 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
|
||||
// whole app whenever the pool is exhausted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlobManager {
|
||||
pool: SqlitePool,
|
||||
readers: SqlitePool,
|
||||
writer: SqlitePool,
|
||||
}
|
||||
|
||||
impl BlobManager {
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
/// `writer` must be a pool with a single connection.
|
||||
pub fn new(readers: SqlitePool, writer: SqlitePool) -> Self {
|
||||
Self { readers, writer }
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> BlobContext {
|
||||
let conn = self.pool.get().expect("Failed to get blob DB connection from pool");
|
||||
BlobContext { conn }
|
||||
/// A read handle from the reader pool.
|
||||
pub fn connect(&self) -> BlobContext<'_> {
|
||||
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.
|
||||
pub struct BlobContext {
|
||||
conn: SqliteConn,
|
||||
}
|
||||
|
||||
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(())
|
||||
/// Read handle for the blob database.
|
||||
pub struct BlobContext<'a> {
|
||||
conn: ConnectionOrTx<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BlobContext<'a> {
|
||||
/// Get all chunks for a body, ordered by chunk_index.
|
||||
pub fn get_chunks(&self, body_id: &str) -> Result<Vec<BodyChunk>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
@@ -87,25 +112,11 @@ impl BlobContext {
|
||||
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.
|
||||
impl BlobContext {
|
||||
pub fn get_body_size(&self, body_id: &str) -> Result<usize> {
|
||||
let size: i64 = self
|
||||
.conn
|
||||
.resolve()
|
||||
.query_row(
|
||||
"SELECT COALESCE(SUM(LENGTH(data)), 0) FROM body_chunks WHERE body_id = ?1",
|
||||
params![body_id],
|
||||
@@ -119,6 +130,7 @@ impl BlobContext {
|
||||
pub fn body_exists(&self, body_id: &str) -> Result<bool> {
|
||||
let count: i64 = self
|
||||
.conn
|
||||
.resolve()
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM body_chunks WHERE body_id = ?1",
|
||||
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.
|
||||
pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> {
|
||||
info!("Running blob database migrations");
|
||||
@@ -196,28 +246,39 @@ pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
|
||||
fn create_test_pool() -> SqlitePool {
|
||||
fn create_test_manager() -> BlobManager {
|
||||
let manager = r2d2_sqlite::SqliteConnectionManager::memory();
|
||||
let pool = r2d2::Pool::builder().max_size(1).build(manager).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]
|
||||
fn test_insert_and_get_chunks() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
let body_id = "rs_test123.request";
|
||||
let chunk1 = BodyChunk::new(body_id, 0, b"Hello, ".to_vec());
|
||||
let chunk2 = BodyChunk::new(body_id, 1, b"World!".to_vec());
|
||||
insert(
|
||||
&manager,
|
||||
&[
|
||||
BodyChunk::new(body_id, 0, b"Hello, ".to_vec()),
|
||||
BodyChunk::new(body_id, 1, b"World!".to_vec()),
|
||||
],
|
||||
);
|
||||
|
||||
ctx.insert_chunk(&chunk1).unwrap();
|
||||
ctx.insert_chunk(&chunk2).unwrap();
|
||||
|
||||
let chunks = ctx.get_chunks(body_id).unwrap();
|
||||
let chunks = manager.connect().get_chunks(body_id).unwrap();
|
||||
assert_eq!(chunks.len(), 2);
|
||||
assert_eq!(chunks[0].chunk_index, 0);
|
||||
assert_eq!(chunks[0].data, b"Hello, ");
|
||||
@@ -227,18 +288,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_chunks_ordered_by_index() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
let body_id = "rs_test123.request";
|
||||
|
||||
// Insert out of order
|
||||
ctx.insert_chunk(&BodyChunk::new(body_id, 2, b"C".to_vec())).unwrap();
|
||||
ctx.insert_chunk(&BodyChunk::new(body_id, 0, b"A".to_vec())).unwrap();
|
||||
ctx.insert_chunk(&BodyChunk::new(body_id, 1, b"B".to_vec())).unwrap();
|
||||
insert(
|
||||
&manager,
|
||||
&[
|
||||
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[0].data, b"A");
|
||||
assert_eq!(chunks[1].data, b"B");
|
||||
@@ -247,89 +309,85 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_delete_chunks() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
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());
|
||||
|
||||
ctx.delete_chunks(body_id).unwrap();
|
||||
manager.with_tx(|b| b.delete_chunks(body_id)).unwrap();
|
||||
|
||||
let ctx = manager.connect();
|
||||
assert!(!ctx.body_exists(body_id).unwrap());
|
||||
assert_eq!(ctx.get_chunks(body_id).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_chunks_like() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
// Insert chunks for same response but different body types
|
||||
ctx.insert_chunk(&BodyChunk::new("rs_abc.request", 0, b"req".to_vec())).unwrap();
|
||||
ctx.insert_chunk(&BodyChunk::new("rs_abc.response", 0, b"resp".to_vec())).unwrap();
|
||||
ctx.insert_chunk(&BodyChunk::new("rs_other.request", 0, b"other".to_vec())).unwrap();
|
||||
insert(
|
||||
&manager,
|
||||
&[
|
||||
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
|
||||
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.response").unwrap());
|
||||
|
||||
// rs_other should still exist
|
||||
assert!(ctx.body_exists("rs_other.request").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_body_size() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
let body_id = "rs_test123.request";
|
||||
ctx.insert_chunk(&BodyChunk::new(body_id, 0, b"Hello".to_vec())).unwrap();
|
||||
ctx.insert_chunk(&BodyChunk::new(body_id, 1, b"World".to_vec())).unwrap();
|
||||
insert(
|
||||
&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
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_body_size_empty() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let size = ctx.get_body_size("nonexistent").unwrap();
|
||||
let manager = create_test_manager();
|
||||
let size = manager.connect().get_body_size("nonexistent").unwrap();
|
||||
assert_eq!(size, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_body_exists() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
let manager = create_test_manager();
|
||||
assert!(!manager.connect().body_exists("rs_test.request").unwrap());
|
||||
|
||||
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!(ctx.body_exists("rs_test.request").unwrap());
|
||||
assert!(manager.connect().body_exists("rs_test.request").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_bodies_isolated() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let manager = create_test_manager();
|
||||
insert(
|
||||
&manager,
|
||||
&[
|
||||
BodyChunk::new("body1", 0, b"data1".to_vec()),
|
||||
BodyChunk::new("body2", 0, b"data2".to_vec()),
|
||||
],
|
||||
);
|
||||
|
||||
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 chunks2 = ctx.get_chunks("body2").unwrap();
|
||||
|
||||
@@ -341,16 +399,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_large_chunk() {
|
||||
let pool = create_test_pool();
|
||||
let manager = BlobManager::new(pool);
|
||||
let ctx = manager.connect();
|
||||
|
||||
let manager = create_test_manager();
|
||||
// 1MB chunk
|
||||
let large_data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
|
||||
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();
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0].data, large_data);
|
||||
|
||||
@@ -3,18 +3,25 @@ use crate::models::{AnyModel, UpsertModelInfo};
|
||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use rusqlite::params;
|
||||
use sea_query::{IntoColumnRef, IntoIden, SimpleExpr};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::sync::mpsc;
|
||||
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(crate) ctx: DbContext<'a>,
|
||||
pub(crate) events_tx: mpsc::Sender<ModelPayload>,
|
||||
}
|
||||
|
||||
impl<'a> ClientDb<'a> {
|
||||
pub fn new(ctx: DbContext<'a>, events_tx: mpsc::Sender<ModelPayload>) -> Self {
|
||||
Self { ctx, events_tx }
|
||||
pub fn new(ctx: DbContext<'a>) -> Self {
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
/// Access the underlying connection for custom queries.
|
||||
@@ -22,8 +29,6 @@ impl<'a> ClientDb<'a> {
|
||||
self.ctx.conn()
|
||||
}
|
||||
|
||||
// --- Read delegates (thin wrappers over DbContext) ---
|
||||
|
||||
pub(crate) fn find_one<M>(
|
||||
&self,
|
||||
col: impl IntoColumnRef + IntoIden + Clone,
|
||||
@@ -64,6 +69,39 @@ impl<'a> ClientDb<'a> {
|
||||
{
|
||||
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
|
||||
/// 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)?)
|
||||
}
|
||||
|
||||
// --- Write operations (with event recording) ---
|
||||
|
||||
pub(crate) fn upsert<M>(&self, model: &M, source: &UpdateSource) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + UpsertModelInfo + Clone,
|
||||
@@ -95,7 +131,7 @@ impl<'a> ClientDb<'a> {
|
||||
};
|
||||
|
||||
self.record_model_change(&payload)?;
|
||||
let _ = self.events_tx.send(payload);
|
||||
self.pending_events.borrow_mut().push(payload);
|
||||
|
||||
Ok(m)
|
||||
}
|
||||
@@ -113,7 +149,7 @@ impl<'a> ClientDb<'a> {
|
||||
};
|
||||
|
||||
self.record_model_change(&payload)?;
|
||||
let _ = self.events_tx.send(payload);
|
||||
self.pending_events.borrow_mut().push(payload);
|
||||
|
||||
Ok(m.clone())
|
||||
}
|
||||
|
||||
@@ -64,6 +64,17 @@ mod open {
|
||||
.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> {
|
||||
let manager = SqliteConnectionManager::memory().with_init(|c| init_connection(c));
|
||||
// In-memory DB doesn't support multiple connections
|
||||
@@ -90,6 +101,16 @@ mod open {
|
||||
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> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
init_connection(&conn)?;
|
||||
@@ -108,27 +129,33 @@ pub fn init_standalone(
|
||||
let db_path = db_path.as_ref();
|
||||
let blob_path = blob_path.as_ref();
|
||||
|
||||
// Main database pool. Sized for concurrent in-flight queries, not concurrent app
|
||||
// features — connections are held per-statement, so even heavy fan-out (e.g. many
|
||||
// gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections
|
||||
// hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit.
|
||||
// Each database gets a reader pool and a one-connection writer pool; see
|
||||
// `QueryManager` for why. Reader pools are sized for concurrent in-flight
|
||||
// queries, not concurrent app features — connections are held per-statement,
|
||||
// so even heavy fan-out (e.g. many gRPC streams) only needs a handful at once.
|
||||
// Keep 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:?}");
|
||||
let pool = open::file_pool(db_path, 20, 2)?;
|
||||
migrate_db(&pool)?;
|
||||
let (readers, writer) = open::file_pools(db_path, 20, 2)?;
|
||||
migrate_db(&writer)?;
|
||||
|
||||
info!("Initializing blobs database {blob_path:?}");
|
||||
let blob_pool = open::file_pool(blob_path, 10, 1)?;
|
||||
migrate_blob_db(&blob_pool)?;
|
||||
let (blob_readers, blob_writer) = open::file_pools(blob_path, 10, 1)?;
|
||||
migrate_blob_db(&blob_writer)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let query_manager = QueryManager::new(pool, tx);
|
||||
let blob_manager = BlobManager::new(blob_pool);
|
||||
let query_manager = QueryManager::new(readers, writer, tx);
|
||||
let blob_manager = BlobManager::new(blob_readers, blob_writer);
|
||||
bootstrap(&query_manager)?;
|
||||
|
||||
Ok((query_manager, blob_manager, rx))
|
||||
}
|
||||
|
||||
/// Initialize the database managers with in-memory SQLite databases.
|
||||
/// 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>)> {
|
||||
let pool = open::memory_pool()?;
|
||||
migrate_db(&pool)?;
|
||||
@@ -137,8 +164,18 @@ pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<Mod
|
||||
migrate_blob_db(&blob_pool)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let query_manager = QueryManager::new(pool, tx);
|
||||
let blob_manager = BlobManager::new(blob_pool);
|
||||
let query_manager = QueryManager::new(pool.clone(), pool, tx);
|
||||
let blob_manager = BlobManager::new(blob_pool.clone(), blob_pool);
|
||||
bootstrap(&query_manager)?;
|
||||
|
||||
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(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
//! caller is a desktop window or an HTTP request.
|
||||
|
||||
use crate::blob_manager::BlobManager;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::WriteDb;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::models::AnyModel;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
pub fn upsert_model(
|
||||
db: &ClientDb,
|
||||
db: &WriteDb,
|
||||
blobs: &BlobManager,
|
||||
model: AnyModel,
|
||||
source: &UpdateSource,
|
||||
@@ -41,7 +41,7 @@ pub fn upsert_model(
|
||||
|
||||
/// Deletes cascade, so callers run this inside a transaction.
|
||||
pub fn delete_model(
|
||||
tx: &ClientDb,
|
||||
tx: &WriteDb,
|
||||
blobs: &BlobManager,
|
||||
model: AnyModel,
|
||||
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
|
||||
/// a duplicate never comes from a stale frontend snapshot.
|
||||
pub fn duplicate_model(
|
||||
tx: &ClientDb,
|
||||
tx: &WriteDb,
|
||||
model_type: &str,
|
||||
model_id: &str,
|
||||
source: &UpdateSource,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::WriteDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace};
|
||||
use crate::util::{BatchUpsertResult, UpdateSource};
|
||||
use log::info;
|
||||
|
||||
impl<'a> ClientDb<'a> {
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn batch_upsert(
|
||||
&self,
|
||||
workspaces: Vec<Workspace>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{CookieJar, CookieJarIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -9,18 +9,22 @@ impl<'a> ClientDb<'a> {
|
||||
}
|
||||
|
||||
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 {
|
||||
name: "Default".to_string(),
|
||||
workspace_id: workspace_id.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
cookie_jars.push(self.upsert_cookie_jar(&jar, &UpdateSource::Background)?);
|
||||
self.upsert_cookie_jar(&jar, &UpdateSource::Background)?;
|
||||
}
|
||||
|
||||
Ok(cookie_jars)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cookie_jar(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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::Result;
|
||||
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> {
|
||||
let environments = self.list_environments_ensure_base(workspace_id)?;
|
||||
let environments = self.list_environments(workspace_id)?;
|
||||
let base_environments = environments
|
||||
.into_iter()
|
||||
.filter(|e| e.parent_model == "workspace")
|
||||
@@ -30,19 +30,85 @@ impl<'a> ClientDb<'a> {
|
||||
return Err(MultipleBaseEnvironments(workspace_id.to_string()));
|
||||
}
|
||||
|
||||
Ok(base_environments.first().cloned().ok_or(
|
||||
// Should never happen because one should be created above if it does not exist
|
||||
MissingBaseEnvironment(workspace_id.to_string()),
|
||||
)?)
|
||||
Ok(base_environments
|
||||
.first()
|
||||
.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_ensure_base(&self, workspace_id: &str) -> Result<Vec<Environment>> {
|
||||
let mut environments = self.list_environments_dangerous(workspace_id)?;
|
||||
pub fn list_environments(&self, workspace_id: &str) -> Result<Vec<Environment>> {
|
||||
Ok(self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?)
|
||||
}
|
||||
|
||||
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(
|
||||
&Environment {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
@@ -53,15 +119,7 @@ impl<'a> ClientDb<'a> {
|
||||
&UpdateSource::Background,
|
||||
)?;
|
||||
info!("Created base environment {} for {workspace_id}", e.id);
|
||||
environments.push(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)?)
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
pub fn delete_environment(
|
||||
@@ -72,7 +130,7 @@ impl<'a> ClientDb<'a> {
|
||||
let deleted_environment = self.delete(environment, source)?;
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -90,7 +148,7 @@ impl<'a> ClientDb<'a> {
|
||||
let mut environment = environment.clone();
|
||||
environment.id = "".to_string();
|
||||
let sibling_names = self
|
||||
.list_environments_dangerous(&environment.workspace_id)?
|
||||
.list_environments(&environment.workspace_id)?
|
||||
.into_iter()
|
||||
.map(|e| e.name)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -98,23 +156,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
environment: &Environment,
|
||||
@@ -154,43 +195,4 @@ impl<'a> ClientDb<'a> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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::error::Result;
|
||||
use crate::models::{
|
||||
@@ -20,99 +20,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
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::models::{GraphQlIntrospection, GraphQlIntrospectionIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
|
||||
pub fn get_graphql_introspection(&self, request_id: &str) -> Option<GraphQlIntrospection> {
|
||||
self.find_optional(GraphQlIntrospectionIden::RequestId, request_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_graphql_introspection(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState};
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
@@ -13,6 +13,20 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
request_id: &str,
|
||||
@@ -53,18 +67,6 @@ impl<'a> ClientDb<'a> {
|
||||
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<()> {
|
||||
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
|
||||
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::models::{GrpcEvent, GrpcEventIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
|
||||
pub fn list_grpc_events(&self, connection_id: &str) -> Result<Vec<GrpcEvent>> {
|
||||
self.find_many(GrpcEventIden::ConnectionId, connection_id, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_grpc_event(
|
||||
&self,
|
||||
grpc_event: &GrpcEvent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
AnyModel, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequestHeader,
|
||||
@@ -32,50 +32,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
AnyModel, Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden,
|
||||
@@ -18,50 +18,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
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)]
|
||||
mod tests {
|
||||
use crate::init_in_memory;
|
||||
@@ -225,43 +227,45 @@ mod tests {
|
||||
#[test]
|
||||
fn http_version_resolves_through_the_inheritance_chain() {
|
||||
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
|
||||
.upsert_workspace(
|
||||
let (folder, request) = query_manager
|
||||
.with_tx(|db| {
|
||||
let workspace = db.upsert_workspace(
|
||||
&Workspace {
|
||||
name: "Test".to_string(),
|
||||
setting_http_version: HttpVersion::Http2,
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.expect("Failed to upsert workspace");
|
||||
|
||||
let folder = db
|
||||
.upsert_folder(
|
||||
source,
|
||||
)?;
|
||||
let folder = db.upsert_folder(
|
||||
&Folder { workspace_id: workspace.id.clone(), ..Default::default() },
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.expect("Failed to upsert folder");
|
||||
|
||||
let request = db
|
||||
.upsert_http_request(
|
||||
source,
|
||||
)?;
|
||||
let request = db.upsert_http_request(
|
||||
&HttpRequest {
|
||||
workspace_id: workspace.id.clone(),
|
||||
folder_id: Some(folder.id.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.expect("Failed to upsert request");
|
||||
source,
|
||||
)?;
|
||||
Ok::<_, crate::error::Error>((folder, request))
|
||||
})
|
||||
.expect("Failed to seed");
|
||||
|
||||
// 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.source_model, "workspace");
|
||||
|
||||
// A folder override beats the workspace base
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
db.upsert_folder(
|
||||
&Folder {
|
||||
setting_http_version: InheritedHttpVersionSetting {
|
||||
@@ -270,16 +274,21 @@ mod tests {
|
||||
},
|
||||
..folder
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
source,
|
||||
)
|
||||
})
|
||||
.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.source_model, "folder");
|
||||
|
||||
// A request override beats them both
|
||||
let request = db
|
||||
.upsert_http_request(
|
||||
let request = query_manager
|
||||
.with_tx(|db| {
|
||||
db.upsert_http_request(
|
||||
&HttpRequest {
|
||||
setting_http_version: InheritedHttpVersionSetting {
|
||||
enabled: true,
|
||||
@@ -287,10 +296,14 @@ mod tests {
|
||||
},
|
||||
..request
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
source,
|
||||
)
|
||||
})
|
||||
.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.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::models::{HttpResponseEvent, HttpResponseEventIden};
|
||||
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>> {
|
||||
self.find_many(HttpResponseEventIden::ResponseId, response_id, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_http_response_event(
|
||||
&self,
|
||||
http_response_event: &HttpResponseEvent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::blob_manager::BlobManager;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState};
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
@@ -31,20 +31,6 @@ impl<'a> ClientDb<'a> {
|
||||
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
|
||||
/// longer exists. Blob ids are keyed by the response that owns them —
|
||||
/// "{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.
|
||||
pub fn delete_orphaned_response_body_blobs(&self, blobs: &BlobManager) -> Result<usize> {
|
||||
let mut deleted = 0;
|
||||
|
||||
let blob_ctx = blobs.connect();
|
||||
for body_id in blob_ctx.list_body_ids()? {
|
||||
let orphaned = blobs
|
||||
.connect()
|
||||
.list_body_ids()?
|
||||
.into_iter()
|
||||
.filter(|body_id| {
|
||||
let response_id = body_id.split('.').next().unwrap_or_default();
|
||||
if self.find_optional::<HttpResponse>(HttpResponseIden::Id, response_id).is_some() {
|
||||
continue;
|
||||
}
|
||||
blob_ctx.delete_chunks(&body_id)?;
|
||||
deleted += 1;
|
||||
}
|
||||
self.find_optional::<HttpResponse>(HttpResponseIden::Id, response_id).is_none()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
@@ -107,6 +98,22 @@ impl<'a> ClientDb<'a> {
|
||||
|
||||
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.
|
||||
pub fn delete_all_http_responses_for_workspace(
|
||||
@@ -137,9 +144,8 @@ impl<'a> ClientDb<'a> {
|
||||
}
|
||||
|
||||
// Delete request body blobs (pattern: {response_id}.request)
|
||||
let blob_ctx = blob_manager.connect();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -186,26 +192,28 @@ impl<'a> ClientDb<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::blob_manager::{BlobManager, BodyChunk};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Error;
|
||||
use crate::init_in_memory;
|
||||
use crate::models::{HttpRequest, HttpResponse, Workspace};
|
||||
use crate::query_manager::QueryManager;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
/// 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 workspace = db
|
||||
.upsert_workspace(
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let workspace = db.upsert_workspace(
|
||||
&Workspace { name: "GC Test".to_string(), ..Default::default() },
|
||||
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() },
|
||||
source,
|
||||
)
|
||||
.expect("Failed to upsert request");
|
||||
)?;
|
||||
db.upsert_http_response(
|
||||
&HttpResponse {
|
||||
request_id: request.id.clone(),
|
||||
@@ -215,7 +223,19 @@ mod tests {
|
||||
source,
|
||||
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
|
||||
@@ -223,23 +243,21 @@ mod tests {
|
||||
#[test]
|
||||
fn deletes_orphaned_response_body_blobs() {
|
||||
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);
|
||||
{
|
||||
// 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.id, 0, b"live".to_vec())).unwrap();
|
||||
blob_ctx
|
||||
.insert_chunk(&BodyChunk::new(&live_request_body_id, 0, b"live".to_vec()))
|
||||
.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();
|
||||
}
|
||||
insert_bodies(
|
||||
&blob_manager,
|
||||
&[
|
||||
&live.id,
|
||||
&live_request_body_id,
|
||||
"rs_gone",
|
||||
"rs_gone.request",
|
||||
],
|
||||
);
|
||||
|
||||
let deleted = db
|
||||
let deleted = query_manager
|
||||
.connect()
|
||||
.delete_orphaned_response_body_blobs(&blob_manager)
|
||||
.expect("Failed to GC response body blobs");
|
||||
assert_eq!(deleted, 2);
|
||||
@@ -254,24 +272,18 @@ mod tests {
|
||||
#[test]
|
||||
fn deletes_orphaned_response_bodies() {
|
||||
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);
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
insert_bodies(&blob_manager, &[&live_body_id, "rs_gone.request"]);
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("yaak-blob-gc-test-{}", live.id));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(&live.id), b"live").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)
|
||||
.expect("Failed to GC response bodies");
|
||||
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::models::{ImportSourceResource, ImportSourceResourceIden};
|
||||
use sea_query::ExprTrait;
|
||||
@@ -20,7 +20,9 @@ impl<'a> ClientDb<'a> {
|
||||
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
|
||||
Ok(items.filter_map(|v| v.ok()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_import_source_resource(
|
||||
&self,
|
||||
resource: &ImportSourceResource,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{ImportSource, ImportSourceIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -25,7 +25,9 @@ impl<'a> ClientDb<'a> {
|
||||
let sources = self.list_import_sources(workspace_id)?;
|
||||
Ok(sources.into_iter().find(|s| s.importer == importer && s.origin == origin))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_import_source(
|
||||
&self,
|
||||
import_source: &ImportSource,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -89,7 +89,9 @@ impl<'a> ClientDb<'a> {
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
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(
|
||||
&self,
|
||||
namespace: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::util::ModelPayload;
|
||||
use rusqlite::params;
|
||||
@@ -69,7 +69,9 @@ impl<'a> ClientDb<'a> {
|
||||
|
||||
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> {
|
||||
let offset = format!("-{days} days");
|
||||
Ok(self.conn().resolve().execute(
|
||||
@@ -101,13 +103,24 @@ mod tests {
|
||||
use crate::util::{ModelChangeEvent, UpdateSource};
|
||||
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]
|
||||
fn records_model_changes_for_upsert_and_delete() {
|
||||
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
|
||||
.upsert_workspace(
|
||||
let workspace = query_manager
|
||||
.with_tx(|db| {
|
||||
db.upsert_workspace(
|
||||
&Workspace {
|
||||
name: "Changes Test".to_string(),
|
||||
setting_follow_redirects: true,
|
||||
@@ -116,8 +129,10 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.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");
|
||||
assert_eq!(created_changes.len(), 1);
|
||||
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));
|
||||
|
||||
drop(db);
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
db.delete_workspace_by_id(&workspace.id, &UpdateSource::Sync, &blob_manager)
|
||||
})
|
||||
.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");
|
||||
assert_eq!(all_changes.len(), 2);
|
||||
assert!(matches!(all_changes[1].payload.change, ModelChangeEvent::Delete));
|
||||
@@ -146,8 +166,10 @@ mod tests {
|
||||
#[test]
|
||||
fn prunes_old_model_changes() {
|
||||
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(
|
||||
&Workspace {
|
||||
name: "Prune Test".to_string(),
|
||||
@@ -157,8 +179,10 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.expect("Failed to upsert workspace");
|
||||
|
||||
let db = query_manager.connect();
|
||||
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
|
||||
assert_eq!(changes.len(), 1);
|
||||
|
||||
@@ -170,19 +194,28 @@ mod tests {
|
||||
)
|
||||
.expect("Failed to age model change row");
|
||||
|
||||
let pruned =
|
||||
db.prune_model_changes_older_than_days(30).expect("Failed to prune model changes");
|
||||
drop(db);
|
||||
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!(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]
|
||||
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 db = query_manager.connect();
|
||||
clear_changes(&query_manager);
|
||||
|
||||
let workspace = db
|
||||
.upsert_workspace(
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let workspace = db.upsert_workspace(
|
||||
&Workspace {
|
||||
name: "Cursor Test".to_string(),
|
||||
setting_follow_redirects: true,
|
||||
@@ -190,11 +223,12 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
.expect("Failed to upsert workspace");
|
||||
)?;
|
||||
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");
|
||||
assert_eq!(all.len(), 2);
|
||||
|
||||
@@ -213,8 +247,10 @@ mod tests {
|
||||
#[test]
|
||||
fn prunes_old_model_changes_by_hours() {
|
||||
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(
|
||||
&Workspace {
|
||||
name: "Prune Hour Test".to_string(),
|
||||
@@ -224,8 +260,10 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.expect("Failed to upsert workspace");
|
||||
|
||||
let db = query_manager.connect();
|
||||
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
|
||||
assert_eq!(changes.len(), 1);
|
||||
|
||||
@@ -237,14 +275,17 @@ mod tests {
|
||||
)
|
||||
.expect("Failed to age model change row");
|
||||
|
||||
let pruned =
|
||||
db.prune_model_changes_older_than_hours(1).expect("Failed to prune model changes");
|
||||
drop(db);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_model_changes_deserializes_http_response_event_payload() {
|
||||
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
|
||||
clear_changes(&query_manager);
|
||||
let db = query_manager.connect();
|
||||
|
||||
let payload = json!({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{PluginKeyValue, PluginKeyValueIden};
|
||||
use sea_query::ExprTrait;
|
||||
@@ -22,7 +22,9 @@ impl<'a> ClientDb<'a> {
|
||||
.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into())
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn set_plugin_key_value(
|
||||
&self,
|
||||
plugin_name: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{Plugin, PluginIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -15,7 +15,9 @@ impl<'a> ClientDb<'a> {
|
||||
pub fn list_plugins(&self) -> Result<Vec<Plugin>> {
|
||||
self.find_all()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn delete_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
||||
self.delete(plugin, source)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{EditorKeymap, Settings, SettingsIden};
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
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 {
|
||||
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) {
|
||||
return s;
|
||||
};
|
||||
impl<'a> WriteDb<'a> {
|
||||
/// 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(),
|
||||
id,
|
||||
id: "default".to_string(),
|
||||
created_at: Default::default(),
|
||||
updated_at: Default::default(),
|
||||
|
||||
@@ -42,11 +57,5 @@ impl<'a> ClientDb<'a> {
|
||||
auto_download_updates: true,
|
||||
check_notifications: true,
|
||||
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::models::{SyncState, SyncStateIden, UpsertModelInfo};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -12,10 +12,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
@@ -34,6 +30,12 @@ impl<'a> ClientDb<'a> {
|
||||
let items = stmt.query_map(&*params.as_params(), SyncState::from_row)?;
|
||||
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> {
|
||||
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::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConnectionState};
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
@@ -13,6 +13,22 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
request_id: &str,
|
||||
@@ -37,20 +53,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
websocket_connection: &WebsocketConnection,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{WebsocketEvent, WebsocketEventIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -11,7 +11,9 @@ impl<'a> ClientDb<'a> {
|
||||
pub fn list_websocket_events(&self, connection_id: &str) -> Result<Vec<WebsocketEvent>> {
|
||||
self.find_many(WebsocketEventIden::ConnectionId, connection_id, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn upsert_websocket_event(
|
||||
&self,
|
||||
websocket_event: &WebsocketEvent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
AnyModel, Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, ResolvedSetting,
|
||||
@@ -34,50 +34,6 @@ impl<'a> ClientDb<'a> {
|
||||
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(
|
||||
&self,
|
||||
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::models::{WorkspaceMeta, WorkspaceMetaIden};
|
||||
use crate::util::UpdateSource;
|
||||
@@ -10,18 +10,12 @@ impl<'a> ClientDb<'a> {
|
||||
}
|
||||
|
||||
pub fn list_workspace_metas(&self, workspace_id: &str) -> Result<Vec<WorkspaceMeta>> {
|
||||
let mut workspace_metas =
|
||||
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)?)
|
||||
self.find_many(WorkspaceMetaIden::WorkspaceId, workspace_id, None)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(workspace_metas)
|
||||
}
|
||||
|
||||
pub fn get_or_create_workspace_meta(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
|
||||
impl<'a> WriteDb<'a> {
|
||||
pub fn ensure_workspace_meta(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
|
||||
let workspace_meta = self.get_workspace_meta(workspace_id);
|
||||
if let Some(workspace_meta) = workspace_meta {
|
||||
return Ok(workspace_meta);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::merge_headers;
|
||||
use crate::blob_manager::BlobManager;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden,
|
||||
@@ -23,119 +23,7 @@ impl<'a> ClientDb<'a> {
|
||||
}
|
||||
|
||||
pub fn list_workspaces(&self) -> Result<Vec<Workspace>> {
|
||||
let mut workspaces = 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)
|
||||
self.find_all()
|
||||
}
|
||||
|
||||
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.
|
||||
/// These are prepended to the inheritance chain so workspace/folder/request headers
|
||||
/// can override or disable them.
|
||||
|
||||
@@ -1,65 +1,82 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::client_db::{ClientDb, WriteDb};
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::util::ModelPayload;
|
||||
use rusqlite::{Transaction, TransactionBehavior};
|
||||
use std::sync::mpsc;
|
||||
use yaak_database::{ConnectionOrTx, DbContext, SqlitePool};
|
||||
|
||||
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
|
||||
// blocking `get()` serializes every DB access behind the slowest waiter, freezing the
|
||||
// whole app whenever the pool is exhausted.
|
||||
/// Reads come from a pool; writes go through one connection.
|
||||
///
|
||||
/// 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)]
|
||||
pub struct QueryManager {
|
||||
pool: SqlitePool,
|
||||
readers: SqlitePool,
|
||||
writer: SqlitePool,
|
||||
events_tx: mpsc::Sender<ModelPayload>,
|
||||
}
|
||||
|
||||
impl QueryManager {
|
||||
pub fn new(pool: SqlitePool, events_tx: mpsc::Sender<ModelPayload>) -> Self {
|
||||
QueryManager { pool, events_tx }
|
||||
/// `writer` must be a pool with a single connection; see [`crate::init_standalone`].
|
||||
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<'_> {
|
||||
let conn = self.pool.get().expect("Failed to get a new DB connection from the pool");
|
||||
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
|
||||
ClientDb::new(ctx, self.events_tx.clone())
|
||||
}
|
||||
|
||||
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)
|
||||
let conn = self.readers.get().expect("Failed to get a new DB connection from the pool");
|
||||
ClientDb::new(DbContext::new(ConnectionOrTx::Connection(conn)))
|
||||
}
|
||||
|
||||
/// 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>(
|
||||
&self,
|
||||
func: impl FnOnce(&ClientDb) -> std::result::Result<T, E>,
|
||||
func: impl FnOnce(&WriteDb) -> std::result::Result<T, E>,
|
||||
) -> std::result::Result<T, E>
|
||||
where
|
||||
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
|
||||
// the pool never hands out `&mut`.
|
||||
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 = ClientDb::new(ctx, self.events_tx.clone());
|
||||
let db =
|
||||
WriteDb::new(DbContext::new(ConnectionOrTx::Transaction(&tx)), self.events_tx.clone());
|
||||
|
||||
match func(&db) {
|
||||
Ok(val) => {
|
||||
let events = db.into_events();
|
||||
tx.commit()
|
||||
.map_err(|e| GenericError(format!("Failed to commit transaction {e:?}")))?;
|
||||
for payload in events {
|
||||
let _ = self.events_tx.send(payload);
|
||||
}
|
||||
Ok(val)
|
||||
}
|
||||
Err(e) => {
|
||||
drop(db);
|
||||
tx.rollback()
|
||||
.map_err(|e| GenericError(format!("Failed to rollback transaction {e:?}")))?;
|
||||
Err(e)
|
||||
|
||||
@@ -277,7 +277,7 @@ pub fn get_workspace_export_resources(
|
||||
data.resources.workspaces.push(db.find_one(WorkspaceIden::Id, workspace_id)?);
|
||||
data.resources.environments.append(
|
||||
&mut db
|
||||
.list_environments_ensure_base(workspace_id)?
|
||||
.list_environments(workspace_id)?
|
||||
.into_iter()
|
||||
.filter(|e| include_private_environments || e.public)
|
||||
.collect(),
|
||||
|
||||
@@ -24,11 +24,7 @@ pub async fn delete_and_uninstall(
|
||||
Some(label) => UpdateSource::from_window_label(label),
|
||||
None => UpdateSource::Background,
|
||||
};
|
||||
// Scope the db connection so it doesn't live across await
|
||||
let plugin = {
|
||||
let db = query_manager.connect();
|
||||
db.delete_plugin_by_id(plugin_id, &update_source)?
|
||||
};
|
||||
let plugin = query_manager.with_tx(|db| db.delete_plugin_by_id(plugin_id, &update_source))?;
|
||||
if let Err(err) = plugin_manager.uninstall(plugin_context, plugin.directory.as_str()).await {
|
||||
if !matches!(err, PluginNotFoundErr(_)) {
|
||||
return Err(err);
|
||||
@@ -72,9 +68,7 @@ pub async fn download_and_install(
|
||||
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
|
||||
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
|
||||
|
||||
// Scope the db connection so it doesn't live across await
|
||||
let plugin = {
|
||||
let db = query_manager.connect();
|
||||
let plugin = query_manager.with_tx(|db| {
|
||||
db.upsert_plugin(
|
||||
&Plugin {
|
||||
id: plugin_version.id.clone(),
|
||||
@@ -86,8 +80,8 @@ pub async fn download_and_install(
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)?
|
||||
};
|
||||
)
|
||||
})?;
|
||||
|
||||
plugin_manager.add_plugin(plugin_context, &plugin).await?;
|
||||
|
||||
|
||||
@@ -187,9 +187,7 @@ impl PluginManager {
|
||||
}
|
||||
|
||||
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 = {
|
||||
let db = query_manager.connect();
|
||||
let plugins = query_manager.with_tx(|db| {
|
||||
for dir in &bundled_dirs {
|
||||
if db.get_plugin_by_directory(dir).is_none() {
|
||||
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;
|
||||
if !init_errors.is_empty() {
|
||||
|
||||
@@ -11,8 +11,11 @@ use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use ts_rs::TS;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::models::{SyncState, WorkspaceMeta};
|
||||
use yaak_models::client_db::{ClientDb, WriteDb};
|
||||
use yaak_models::models::{
|
||||
Environment, Folder, GrpcRequest, HttpRequest, SyncState, WebsocketRequest, Workspace,
|
||||
WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::util::{UpdateSource, get_workspace_export_resources};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
@@ -336,17 +339,40 @@ fn workspace_models(db: &ClientDb, version: &str, workspace_id: &str) -> Result<
|
||||
Ok(sync_models)
|
||||
}
|
||||
|
||||
/// Apply sync operations to the filesystem and database.
|
||||
/// Returns a list of SyncStateOps that should be applied afterward.
|
||||
pub fn apply_sync_ops(
|
||||
db: &ClientDb,
|
||||
blobs: &BlobManager,
|
||||
/// The database half of a sync apply, ready to run once the files are on disk.
|
||||
pub struct PendingDbSyncOps {
|
||||
sync_state_ops: Vec<SyncStateOp>,
|
||||
deletes: Vec<SyncModel>,
|
||||
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,
|
||||
sync_dir: &Path,
|
||||
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() {
|
||||
return Ok(Vec::new());
|
||||
return Ok(pending);
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -354,21 +380,13 @@ pub fn apply_sync_ops(
|
||||
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 {
|
||||
// Only apply things if workspace ID matches
|
||||
if op.workspace_id() != workspace_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
sync_state_ops.push(match op {
|
||||
let state_op = match op {
|
||||
SyncOp::FsCreate { model } => {
|
||||
let rel_path = derive_model_filename(&model);
|
||||
let abs_path = sync_dir.join(rel_path.clone());
|
||||
@@ -402,17 +420,7 @@ pub fn apply_sync_ops(
|
||||
},
|
||||
SyncOp::DbCreate { fs } => {
|
||||
let model_id = fs.model.id();
|
||||
|
||||
// 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),
|
||||
};
|
||||
pending.push_upsert(fs.model);
|
||||
SyncStateOp::Create {
|
||||
model_id,
|
||||
checksum: fs.checksum.to_owned(),
|
||||
@@ -420,16 +428,7 @@ pub fn apply_sync_ops(
|
||||
}
|
||||
}
|
||||
SyncOp::DbUpdate { state, fs } => {
|
||||
// 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),
|
||||
}
|
||||
pending.push_upsert(fs.model);
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum: fs.checksum.to_owned(),
|
||||
@@ -437,20 +436,52 @@ pub fn apply_sync_ops(
|
||||
}
|
||||
}
|
||||
SyncOp::DbDelete { model, state } => {
|
||||
delete_model(db, blobs, &model)?;
|
||||
pending.deletes.push(model);
|
||||
SyncStateOp::Delete { state: state.to_owned() }
|
||||
}
|
||||
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(
|
||||
workspaces_to_upsert,
|
||||
environments_to_upsert,
|
||||
folders_to_upsert,
|
||||
http_requests_to_upsert,
|
||||
grpc_requests_to_upsert,
|
||||
websocket_requests_to_upsert,
|
||||
pending.workspaces,
|
||||
pending.environments,
|
||||
pending.folders,
|
||||
pending.http_requests,
|
||||
pending.grpc_requests,
|
||||
pending.websocket_requests,
|
||||
&UpdateSource::Sync,
|
||||
)?;
|
||||
|
||||
@@ -482,7 +513,7 @@ pub fn apply_sync_ops(
|
||||
}?;
|
||||
}
|
||||
|
||||
Ok(sync_state_ops)
|
||||
Ok(pending.sync_state_ops)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -504,7 +535,7 @@ pub enum SyncStateOp {
|
||||
}
|
||||
|
||||
pub fn apply_sync_state_ops(
|
||||
db: &ClientDb,
|
||||
db: &WriteDb,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
ops: Vec<SyncStateOp>,
|
||||
@@ -549,7 +580,7 @@ fn derive_model_filename(m: &SyncModel) -> PathBuf {
|
||||
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 {
|
||||
SyncModel::Workspace(m) => {
|
||||
db.delete_workspace(&m, &UpdateSource::Sync, blobs)?;
|
||||
|
||||
+50
-26
@@ -104,7 +104,7 @@ pub async fn boot() -> Result<()> {
|
||||
let (queries, blobs, events) =
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -266,10 +266,17 @@ fn dispatch(
|
||||
|
||||
if let Some(wid) = req.workspace_id.as_deref() {
|
||||
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_environments_ensure_base(wid).map_err(e)?.into_iter().map(Into::into),
|
||||
);
|
||||
list.extend(db.list_environments(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_requests(wid).map_err(e)?.into_iter().map(Into::into));
|
||||
@@ -291,9 +298,10 @@ fn dispatch(
|
||||
|
||||
"models_upsert" => {
|
||||
let req: ModelReq = from_js(payload)?;
|
||||
let db = host.queries.connect();
|
||||
let id =
|
||||
models_ops::upsert_model(&db, &host.blobs, req.model, source).map_err(js_error)?;
|
||||
let id = host
|
||||
.queries
|
||||
.with_tx(|tx| models_ops::upsert_model(tx, &host.blobs, req.model, source))
|
||||
.map_err(js_error)?;
|
||||
to_json(id)
|
||||
}
|
||||
|
||||
@@ -330,13 +338,14 @@ fn dispatch(
|
||||
let req: UpsertIntrospectionReq = from_js(payload)?;
|
||||
let saved = host
|
||||
.queries
|
||||
.connect()
|
||||
.upsert_graphql_introspection(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_graphql_introspection(
|
||||
&req.workspace_id,
|
||||
&req.request_id,
|
||||
req.content,
|
||||
source,
|
||||
)
|
||||
})
|
||||
.map_err(js_error)?;
|
||||
to_json(saved)
|
||||
}
|
||||
@@ -366,10 +375,14 @@ fn dispatch(
|
||||
if req.before == req.after {
|
||||
return to_json(());
|
||||
}
|
||||
let db = host.queries.connect();
|
||||
let jar = db.get_cookie_jar(&req.cookie_jar_id).map_err(js_error)?;
|
||||
let cookies = apply_cookie_changes(jar.cookies.clone(), &req.before, &req.after);
|
||||
db.upsert_cookie_jar(&CookieJar { cookies, ..jar }, source).map_err(js_error)?;
|
||||
host.queries
|
||||
.with_tx(|tx| {
|
||||
let jar = tx.get_cookie_jar(&req.cookie_jar_id)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -378,26 +391,34 @@ fn dispatch(
|
||||
// writes fan out to every tab as `model_writes` like any other.
|
||||
"web_insert_http_response_events" => {
|
||||
let req: InsertResponseEventsReq = from_js(payload)?;
|
||||
let db = host.queries.connect();
|
||||
host.queries
|
||||
.with_tx(|tx| {
|
||||
for event in req.events {
|
||||
let model = HttpResponseEvent::new(&req.response_id, &req.workspace_id, event);
|
||||
db.upsert_http_response_event(&model, source).map_err(js_error)?;
|
||||
let model =
|
||||
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(())
|
||||
}
|
||||
|
||||
"cmd_get_workspace_meta" => {
|
||||
let req: WorkspaceIdReq = from_js(payload)?;
|
||||
let db = host.queries.connect();
|
||||
let workspace = db.get_workspace(&req.workspace_id).map_err(js_error)?;
|
||||
to_json(db.get_or_create_workspace_meta(&workspace.id).map_err(js_error)?)
|
||||
let workspace =
|
||||
host.queries.connect().get_workspace(&req.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" => {
|
||||
let req: RequestIdReq = from_js(payload)?;
|
||||
host.queries
|
||||
.connect()
|
||||
.delete_all_http_responses_for_request(&req.request_id, source)
|
||||
.with_tx(|tx| tx.delete_all_http_responses_for_request(&req.request_id, source))
|
||||
.map_err(js_error)?;
|
||||
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<()> {
|
||||
const CHUNK: usize = 512 * 1024;
|
||||
with_host(|host| {
|
||||
let ctx = host.blobs.connect();
|
||||
ctx.delete_chunks(id).map_err(js_error)?;
|
||||
host.blobs
|
||||
.with_tx(|b| {
|
||||
b.delete_chunks(id)?;
|
||||
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]
|
||||
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
@@ -4,7 +4,7 @@ use log::info;
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::client_db::{ClientDb, WriteDb};
|
||||
use yaak_models::models::{
|
||||
AnyModel, DEFAULT_REQUEST_MESSAGE_SIZE, Environment, Folder, GrpcRequest, HttpRequest,
|
||||
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> =
|
||||
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.
|
||||
fn delete_existing_model(db: &ClientDb, resource: ImportResourceType, id: &str) -> Result<()> {
|
||||
fn delete_existing_model(db: &WriteDb, resource: ImportResourceType, id: &str) -> Result<()> {
|
||||
use ImportResourceType::*;
|
||||
let source = &UpdateSource::Import;
|
||||
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
|
||||
/// model, so it is neither re-offered nor resurrected.
|
||||
fn record_import_source(
|
||||
db: &ClientDb,
|
||||
db: &WriteDb,
|
||||
plan: &ImportPlan,
|
||||
items: &BTreeMap<String, ImportPlanItem>,
|
||||
upserted: &BatchUpsertResult,
|
||||
@@ -1619,13 +1619,10 @@ mod tests {
|
||||
name: "Selected Folder".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
destination = db
|
||||
.upsert_workspace(&destination, &UpdateSource::Import)
|
||||
.expect("create destination");
|
||||
db.upsert_folder(&selected_folder, &UpdateSource::Import)
|
||||
.expect("create selected folder");
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
destination = db.upsert_workspace(&destination, &UpdateSource::Import)?;
|
||||
db.upsert_folder(&selected_folder, &UpdateSource::Import)?;
|
||||
db.upsert_environment(
|
||||
&Environment {
|
||||
id: "ev_destination_base".to_string(),
|
||||
@@ -1643,8 +1640,9 @@ mod tests {
|
||||
},
|
||||
&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(
|
||||
&query_manager,
|
||||
@@ -1662,13 +1660,10 @@ mod tests {
|
||||
// Planning performed only reads.
|
||||
{
|
||||
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!(db.list_http_requests(&destination.id).expect("list requests").is_empty());
|
||||
assert_eq!(
|
||||
db.list_environments_ensure_base(&destination.id).expect("list environments").len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(db.list_environments(&destination.id).expect("list environments").len(), 1);
|
||||
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");
|
||||
let destination = destination_workspace();
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_workspace(&destination, &UpdateSource::Import)
|
||||
.with_tx(|tx| tx.upsert_workspace(&destination, &UpdateSource::Import))
|
||||
.expect("create destination");
|
||||
let resources = ImportResources {
|
||||
workspaces: vec![
|
||||
@@ -2247,10 +2241,7 @@ mod tests {
|
||||
let db = query_manager.connect();
|
||||
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_environments_ensure_base(&workspace_id).expect("list environments").len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(db.list_environments(&workspace_id).expect("list environments").len(), 1);
|
||||
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");
|
||||
}
|
||||
@@ -2269,11 +2260,10 @@ mod tests {
|
||||
.id
|
||||
.clone();
|
||||
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let nested = db
|
||||
.list_http_requests(&workspace_id)
|
||||
.expect("list requests")
|
||||
.list_http_requests(&workspace_id)?
|
||||
.into_iter()
|
||||
.find(|r| r.name == "Nested Request")
|
||||
.expect("nested request");
|
||||
@@ -2284,8 +2274,8 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
})
|
||||
.expect("edit nested request locally");
|
||||
}
|
||||
|
||||
let mut resources = imported_resources();
|
||||
resources.http_requests[0].url = "https://example.com/root-v2".to_string();
|
||||
@@ -2370,15 +2360,15 @@ mod tests {
|
||||
.id
|
||||
.clone();
|
||||
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
let root = db.get_http_request(&root_id).expect("get root");
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let root = db.get_http_request(&root_id)?;
|
||||
db.upsert_http_request(
|
||||
&HttpRequest { url: "https://example.com/root-local".to_string(), ..root },
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
})
|
||||
.expect("edit root locally");
|
||||
}
|
||||
|
||||
let mut resources = imported_resources();
|
||||
resources.http_requests[0].url = "https://example.com/root-v2".to_string();
|
||||
@@ -2507,8 +2497,7 @@ mod tests {
|
||||
.clone();
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.delete_http_request_by_id(&root_id, &UpdateSource::Background)
|
||||
.with_tx(|tx| tx.delete_http_request_by_id(&root_id, &UpdateSource::Background))
|
||||
.expect("delete root locally");
|
||||
|
||||
let plan = replan(&query_manager, &workspace_id, imported_resources());
|
||||
@@ -2703,10 +2692,9 @@ mod tests {
|
||||
let workspace_id = committed.workspaces[0].id.clone();
|
||||
|
||||
// A second source claiming the same keys leaves nothing to merge into safely.
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
let other = db
|
||||
.upsert_import_source(
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let other = db.upsert_import_source(
|
||||
&ImportSource {
|
||||
workspace_id: workspace_id.clone(),
|
||||
importer: "OpenAPI".to_string(),
|
||||
@@ -2715,18 +2703,18 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Import,
|
||||
)
|
||||
.expect("create second source");
|
||||
)?;
|
||||
for key in ["env:base", "folder:src", "op:root", "op:nested"] {
|
||||
db.upsert_import_source_resource(&ImportSourceResource {
|
||||
import_source_id: other.id.clone(),
|
||||
source_key: key.to_string(),
|
||||
model_type: "http_request".to_string(),
|
||||
..Default::default()
|
||||
})?;
|
||||
}
|
||||
Ok::<_, yaak_models::error::Error>(())
|
||||
})
|
||||
.expect("claim the same keys");
|
||||
}
|
||||
}
|
||||
|
||||
let third =
|
||||
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 workspace_id = committed.workspaces[0].id.clone();
|
||||
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
let sources = db.list_import_sources(&workspace_id).expect("list import sources");
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let sources = db.list_import_sources(&workspace_id)?;
|
||||
let row = db
|
||||
.list_import_source_resources(&sources[0].id)
|
||||
.expect("list rows")
|
||||
.list_import_source_resources(&sources[0].id)?
|
||||
.into_iter()
|
||||
.find(|r| r.source_key == "op:root")
|
||||
.expect("row for the root request");
|
||||
@@ -2834,8 +2821,8 @@ mod tests {
|
||||
content_hash: Some("v99:from-the-future".to_string()),
|
||||
..row
|
||||
})
|
||||
})
|
||||
.expect("write an unreadable hash");
|
||||
}
|
||||
|
||||
let plan = replan(&query_manager, &workspace_id, imported_resources());
|
||||
assert_eq!(
|
||||
@@ -2874,17 +2861,20 @@ mod tests {
|
||||
.clone();
|
||||
|
||||
// Opening the request in the editor stamps a row ID onto every header it renders.
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
let root = db.get_http_request(&root_id).expect("get root");
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let root = db.get_http_request(&root_id)?;
|
||||
let headers = root
|
||||
.headers
|
||||
.iter()
|
||||
.map(|h| HttpRequestHeader { id: Some("hd_generated".to_string()), ..h.clone() })
|
||||
.map(|h| HttpRequestHeader {
|
||||
id: Some("hd_generated".to_string()),
|
||||
..h.clone()
|
||||
})
|
||||
.collect();
|
||||
db.upsert_http_request(&HttpRequest { headers, ..root }, &UpdateSource::Background)
|
||||
})
|
||||
.expect("stamp row ids");
|
||||
}
|
||||
|
||||
let plan = replan(&query_manager, &workspace_id, resources);
|
||||
assert_eq!(
|
||||
@@ -2997,15 +2987,15 @@ mod tests {
|
||||
.id
|
||||
.clone();
|
||||
|
||||
{
|
||||
let db = query_manager.connect();
|
||||
let root = db.get_http_request(&root_id).expect("get root");
|
||||
query_manager
|
||||
.with_tx(|db| {
|
||||
let root = db.get_http_request(&root_id)?;
|
||||
db.upsert_http_request(
|
||||
&HttpRequest { url: "https://example.com/root-local".to_string(), ..root },
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
})
|
||||
.expect("edit root locally");
|
||||
}
|
||||
|
||||
let mut plan = replan(&query_manager, &workspace_id, imported_resources());
|
||||
let root = item_by_name(&plan, "Root Request");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::response_body::ResponseBodyStore;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use log::warn;
|
||||
use yaak_models::models::AnyModel;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -226,11 +227,18 @@ fn build_shared_reply(
|
||||
InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })
|
||||
}
|
||||
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 {})
|
||||
}
|
||||
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) => {
|
||||
InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted })
|
||||
}
|
||||
@@ -331,7 +339,9 @@ fn build_shared_reply(
|
||||
|
||||
let model = match &req.model {
|
||||
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),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -341,7 +351,9 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
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),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -351,7 +363,8 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
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),
|
||||
Err(err) => {
|
||||
@@ -362,7 +375,7 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
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),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -372,7 +385,9 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
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),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -382,7 +397,8 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
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),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -404,8 +420,7 @@ fn build_shared_reply(
|
||||
let model = match req.model.as_str() {
|
||||
"http_request" => {
|
||||
match query_manager
|
||||
.connect()
|
||||
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)
|
||||
.with_tx(|tx| tx.delete_http_request_by_id(&req.id, &UpdateSource::Plugin))
|
||||
{
|
||||
Ok(model) => AnyModel::HttpRequest(model),
|
||||
Err(err) => {
|
||||
@@ -417,8 +432,7 @@ fn build_shared_reply(
|
||||
}
|
||||
"grpc_request" => {
|
||||
match query_manager
|
||||
.connect()
|
||||
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)
|
||||
.with_tx(|tx| tx.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin))
|
||||
{
|
||||
Ok(model) => AnyModel::GrpcRequest(model),
|
||||
Err(err) => {
|
||||
@@ -429,10 +443,9 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
"websocket_request" => {
|
||||
match query_manager
|
||||
.connect()
|
||||
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
|
||||
{
|
||||
match query_manager.with_tx(|tx| {
|
||||
tx.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
|
||||
}) {
|
||||
Ok(model) => AnyModel::WebsocketRequest(model),
|
||||
Err(err) => {
|
||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -442,8 +455,7 @@ fn build_shared_reply(
|
||||
}
|
||||
}
|
||||
"folder" => match query_manager
|
||||
.connect()
|
||||
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)
|
||||
.with_tx(|tx| tx.delete_folder_by_id(&req.id, &UpdateSource::Plugin))
|
||||
{
|
||||
Ok(model) => AnyModel::Folder(model),
|
||||
Err(err) => {
|
||||
@@ -454,8 +466,7 @@ fn build_shared_reply(
|
||||
},
|
||||
"environment" => {
|
||||
match query_manager
|
||||
.connect()
|
||||
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)
|
||||
.with_tx(|tx| tx.delete_environment_by_id(&req.id, &UpdateSource::Plugin))
|
||||
{
|
||||
Ok(model) => AnyModel::Environment(model),
|
||||
Err(err) => {
|
||||
@@ -508,20 +519,16 @@ mod tests {
|
||||
yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB");
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_workspace(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_workspace(
|
||||
&Workspace {
|
||||
id: "wk_test".to_string(),
|
||||
name: "Workspace".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
.expect("Failed to seed workspace");
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_folder(
|
||||
)?;
|
||||
tx.upsert_folder(
|
||||
&Folder {
|
||||
id: "fl_test".to_string(),
|
||||
workspace_id: "wk_test".to_string(),
|
||||
@@ -529,12 +536,8 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
.expect("Failed to seed folder");
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_http_request(
|
||||
)?;
|
||||
tx.upsert_http_request(
|
||||
&HttpRequest {
|
||||
id: "rq_test".to_string(),
|
||||
workspace_id: "wk_test".to_string(),
|
||||
@@ -546,7 +549,8 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
.expect("Failed to seed request");
|
||||
})
|
||||
.expect("Failed to seed");
|
||||
|
||||
(query_manager, temp_dir)
|
||||
}
|
||||
|
||||
@@ -126,16 +126,17 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_workspace(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_workspace(
|
||||
&Workspace { id: "wk_test".to_string(), ..Default::default() },
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_http_request(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_request(
|
||||
&HttpRequest {
|
||||
id: "rq_test".to_string(),
|
||||
workspace_id: "wk_test".to_string(),
|
||||
@@ -143,6 +144,7 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let body_path = body.map(|bytes| {
|
||||
@@ -153,8 +155,8 @@ mod tests {
|
||||
});
|
||||
|
||||
let response = query_manager
|
||||
.connect()
|
||||
.upsert_http_response(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(
|
||||
&HttpResponse {
|
||||
workspace_id: "wk_test".to_string(),
|
||||
request_id: "rq_test".to_string(),
|
||||
@@ -168,6 +170,7 @@ mod tests {
|
||||
&UpdateSource::Sync,
|
||||
&blob_manager,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let id = response.id.clone();
|
||||
@@ -209,7 +212,7 @@ mod tests {
|
||||
|
||||
let mut response = qm.connect().get_http_response(&id).unwrap();
|
||||
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);
|
||||
}
|
||||
|
||||
+44
-36
@@ -646,8 +646,9 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
if let Some(store) = store {
|
||||
response = store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(&response, &store.update_source, store.blob_manager)
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(&response, &store.update_source, store.blob_manager)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||
} else if response.id.is_empty() {
|
||||
response.id = generate_prefixed_id("rs");
|
||||
@@ -700,8 +701,8 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
&event_workspace_id,
|
||||
event.clone().into(),
|
||||
);
|
||||
if let Err(err) =
|
||||
query_manager.connect().upsert_http_response_event(&db_event, update_source)
|
||||
if let Err(err) = query_manager
|
||||
.with_tx(|tx| tx.upsert_http_response_event(&db_event, update_source))
|
||||
{
|
||||
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 {
|
||||
response = store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(&connected_response, &store.update_source, store.blob_manager)
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(
|
||||
&connected_response,
|
||||
&store.update_source,
|
||||
store.blob_manager,
|
||||
)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||
} else {
|
||||
response = connected_response;
|
||||
@@ -886,12 +892,13 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
if let Some(store) = store {
|
||||
response = store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(
|
||||
&progress_response,
|
||||
&store.update_source,
|
||||
store.blob_manager,
|
||||
)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||
} else {
|
||||
response = progress_response;
|
||||
@@ -960,8 +967,9 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
if let Some(store) = store {
|
||||
response = store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(&final_response, &store.update_source, store.blob_manager)
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(&final_response, &store.update_source, store.blob_manager)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||
} else {
|
||||
response = final_response;
|
||||
@@ -998,8 +1006,9 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
if update_response && let Some(store) = store {
|
||||
response = store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(&response, &store.update_source, store.blob_manager)
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(&response, &store.update_source, store.blob_manager)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||
}
|
||||
}
|
||||
@@ -1027,17 +1036,14 @@ fn persist_request_body_bytes(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let blob_ctx = blob_manager.connect();
|
||||
let mut offset = 0;
|
||||
let mut chunk_index: i32 = 0;
|
||||
while offset < bytes.len() {
|
||||
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;
|
||||
blob_manager
|
||||
.with_tx(|b| {
|
||||
for (chunk_index, data) in bytes.chunks(REQUEST_BODY_CHUNK_SIZE).enumerate() {
|
||||
b.insert_chunk(&BodyChunk::new(body_id, chunk_index as i32, data.to_vec()))?;
|
||||
}
|
||||
Ok(())
|
||||
Ok::<_, yaak_models::error::Error>(())
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn persist_request_body_stream(
|
||||
@@ -1057,14 +1063,14 @@ async fn persist_request_body_stream(
|
||||
while buf.len() >= REQUEST_BODY_CHUNK_SIZE {
|
||||
let data = buf.drain(..REQUEST_BODY_CHUNK_SIZE).collect();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if !buf.is_empty() {
|
||||
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)
|
||||
@@ -1114,8 +1120,7 @@ pub fn persist_cookies_after_send(
|
||||
|
||||
cookie_jar.cookies = cookies;
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_cookie_jar(cookie_jar, &UpdateSource::Background)
|
||||
.with_tx(|tx| tx.upsert_cookie_jar(cookie_jar, &UpdateSource::Background))
|
||||
.map_err(SendHttpRequestError::PersistCookieJar)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1209,8 +1214,8 @@ fn persist_response_error(
|
||||
let elapsed = duration_to_i32(started_at.elapsed());
|
||||
store
|
||||
.query_manager
|
||||
.connect()
|
||||
.upsert_http_response(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_http_response(
|
||||
&HttpResponse {
|
||||
state: HttpResponseState::Closed,
|
||||
elapsed,
|
||||
@@ -1226,6 +1231,7 @@ fn persist_response_error(
|
||||
&store.update_source,
|
||||
store.blob_manager,
|
||||
)
|
||||
})
|
||||
.map_err(SendHttpRequestError::PersistResponse)
|
||||
}
|
||||
|
||||
@@ -1444,15 +1450,16 @@ mod tests {
|
||||
.expect("Failed to initialize DB");
|
||||
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_workspace(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_workspace(
|
||||
&Workspace { id: "wk_test".to_string(), ..Default::default() },
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.expect("Failed to seed workspace");
|
||||
let cookie_jar = query_manager
|
||||
.connect()
|
||||
.upsert_cookie_jar(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_cookie_jar(
|
||||
&CookieJar {
|
||||
id: "cj_test".to_string(),
|
||||
workspace_id: "wk_test".to_string(),
|
||||
@@ -1461,6 +1468,7 @@ mod tests {
|
||||
},
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.expect("Failed to seed cookie jar");
|
||||
|
||||
(query_manager, cookie_jar, temp_dir)
|
||||
@@ -1507,18 +1515,18 @@ mod tests {
|
||||
let (query_manager, mut cookie_jar, _temp_dir) = seed_cookie_jar();
|
||||
cookie_jar.cookies = vec![cookie("original")];
|
||||
cookie_jar = query_manager
|
||||
.connect()
|
||||
.upsert_cookie_jar(&cookie_jar, &UpdateSource::Sync)
|
||||
.with_tx(|tx| tx.upsert_cookie_jar(&cookie_jar, &UpdateSource::Sync))
|
||||
.expect("Failed to seed cookies");
|
||||
let store = CookieStore::from_cookies(cookie_jar.cookies.clone());
|
||||
|
||||
// Someone else updates the jar while the send is in flight.
|
||||
query_manager
|
||||
.connect()
|
||||
.upsert_cookie_jar(
|
||||
.with_tx(|tx| {
|
||||
tx.upsert_cookie_jar(
|
||||
&CookieJar { cookies: vec![cookie("newer")], ..cookie_jar.clone() },
|
||||
&UpdateSource::Sync,
|
||||
)
|
||||
})
|
||||
.expect("Failed to update cookie jar");
|
||||
|
||||
persist_cookies_after_send(&query_manager, Some(&mut cookie_jar), Some(&store))
|
||||
|
||||
Reference in New Issue
Block a user