From 18b983bfe5ceda3b103afb374b9fd094d6976f74 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 29 Jun 2026 11:43:20 -0700 Subject: [PATCH] Add CLI import and export commands (#484) --- Cargo.lock | 2 + crates-cli/yaak-cli/Cargo.toml | 1 + crates-cli/yaak-cli/src/cli.rs | 34 ++++ .../yaak-cli/src/commands/import_export.rs | 176 ++++++++++++++++++ crates-cli/yaak-cli/src/commands/mod.rs | 1 + crates-cli/yaak-cli/src/main.rs | 17 ++ .../yaak-cli/tests/import_export_commands.rs | 162 ++++++++++++++++ crates-tauri/yaak-app-client/src/error.rs | 3 + crates-tauri/yaak-app-client/src/import.rs | 119 ++---------- crates-tauri/yaak-app-client/src/lib.rs | 30 +-- crates/yaak/Cargo.toml | 1 + crates/yaak/src/error.rs | 12 ++ crates/yaak/src/export.rs | 29 +++ crates/yaak/src/import.rs | 129 +++++++++++++ crates/yaak/src/lib.rs | 2 + 15 files changed, 592 insertions(+), 126 deletions(-) create mode 100644 crates-cli/yaak-cli/src/commands/import_export.rs create mode 100644 crates-cli/yaak-cli/tests/import_export_commands.rs create mode 100644 crates/yaak/src/export.rs create mode 100644 crates/yaak/src/import.rs diff --git a/Cargo.lock b/Cargo.lock index 41a3a94f..3a514062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10052,6 +10052,7 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", + "yaak-core", "yaak-crypto", "yaak-http", "yaak-models", @@ -10182,6 +10183,7 @@ dependencies = [ "webbrowser", "yaak", "yaak-api", + "yaak-core", "yaak-crypto", "yaak-http", "yaak-models", diff --git a/crates-cli/yaak-cli/Cargo.toml b/crates-cli/yaak-cli/Cargo.toml index b3cb4b73..e8a7993c 100644 --- a/crates-cli/yaak-cli/Cargo.toml +++ b/crates-cli/yaak-cli/Cargo.toml @@ -42,6 +42,7 @@ webbrowser = "1" zip = "4" yaak = { workspace = true } yaak-api = { workspace = true } +yaak-core = { workspace = true } yaak-crypto = { workspace = true } yaak-http = { workspace = true } yaak-models = { workspace = true } diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index bdd77252..c226160e 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -42,6 +42,12 @@ pub enum Commands { /// Authentication commands Auth(AuthArgs), + /// Import API data from Yaak, OpenAPI, Postman, Insomnia, Swagger, or cURL + Import(ImportArgs), + + /// Export Yaak workspace data + Export(ExportArgs), + /// Plugin development and publishing commands Plugin(PluginArgs), @@ -92,6 +98,34 @@ pub struct SendArgs { pub fail_fast: bool, } +#[derive(Args)] +pub struct ImportArgs { + /// Path to the file to import + pub file: PathBuf, + + /// Existing workspace ID to import into when supported by the importer + #[arg(long = "workspace-id", value_name = "WORKSPACE_ID")] + pub workspace_id: Option, +} + +#[derive(Args)] +pub struct ExportArgs { + /// Path to write the Yaak export JSON file + pub file: PathBuf, + + /// Workspace IDs to export (defaults to the only workspace when exactly one exists) + #[arg(value_name = "WORKSPACE_ID")] + pub workspace_ids: Vec, + + /// Export all workspaces + #[arg(long, conflicts_with = "workspace_ids")] + pub all: bool, + + /// Include private environments in the export + #[arg(long)] + pub include_private_environments: bool, +} + #[derive(Args)] #[command(disable_help_subcommand = true)] pub struct CookieJarArgs { diff --git a/crates-cli/yaak-cli/src/commands/import_export.rs b/crates-cli/yaak-cli/src/commands/import_export.rs new file mode 100644 index 00000000..9c763b45 --- /dev/null +++ b/crates-cli/yaak-cli/src/commands/import_export.rs @@ -0,0 +1,176 @@ +use crate::cli::{ExportArgs, ImportArgs}; +use crate::context::CliContext; +use crate::utils::workspace::resolve_workspace_id; +use std::fs; +use std::io::ErrorKind; +use yaak::export::{self, ExportDataParams}; +use yaak::import; +use yaak_core::WorkspaceContext; +use yaak_models::util::BatchUpsertResult; +use yaak_plugins::events::{ImportResources, PluginContext}; + +type CommandResult = std::result::Result; + +pub async fn run_import(ctx: &CliContext, args: ImportArgs) -> i32 { + match import(ctx, args).await { + Ok(result) => { + println!("Imported {}", format_counts(&result)); + 0 + } + Err(error) => { + eprintln!("Error: {error}"); + 1 + } + } +} + +pub fn run_export(ctx: &CliContext, args: ExportArgs) -> i32 { + match export(ctx, args) { + Ok(count) => { + println!("Exported {count} workspace(s)"); + 0 + } + Err(error) => { + eprintln!("Error: {error}"); + 1 + } + } +} + +async fn import(ctx: &CliContext, args: ImportArgs) -> CommandResult { + if let Some(workspace_id) = args.workspace_id.as_deref() { + ctx.db() + .get_workspace(workspace_id) + .map_err(|e| format!("Failed to get workspace '{workspace_id}': {e}"))?; + } + + let file_contents = read_import_file(&args.file)?; + let plugin_context = PluginContext::new(None, args.workspace_id.clone()); + let plugin_manager = ctx.plugin_manager(); + let import_result = plugin_manager + .import_data(&plugin_context, &file_contents) + .await + .map_err(|e| format!("Failed to import data: {e}"))?; + let resources = import_result.resources; + let workspace_id = args.workspace_id; + if workspace_id.is_none() && resources_need_current_workspace(&resources) { + return Err( + "This import requires a workspace context. Provide --workspace-id ." + .to_string(), + ); + } + let workspace_context = WorkspaceContext { + workspace_id, + environment_id: None, + cookie_jar_id: None, + request_id: None, + }; + let imported = import::import_resources(ctx.query_manager(), workspace_context, resources) + .map_err(|e| format!("Failed to import data: {e}"))?; + Ok(imported) +} + +fn export(ctx: &CliContext, args: ExportArgs) -> CommandResult { + let workspace_ids = resolve_export_workspace_ids(ctx, args.workspace_ids, args.all)?; + let workspace_id_refs: Vec<&str> = workspace_ids.iter().map(String::as_str).collect(); + export::export_data(ExportDataParams { + query_manager: ctx.query_manager(), + yaak_version: env!("CARGO_PKG_VERSION"), + export_path: &args.file, + workspace_ids: workspace_id_refs, + include_private_environments: args.include_private_environments, + }) + .map_err(|e| format!("Failed to export data: {e}"))?; + + Ok(workspace_ids.len()) +} + +fn resolve_export_workspace_ids( + ctx: &CliContext, + workspace_ids: Vec, + all: bool, +) -> CommandResult> { + if all { + let workspaces = + ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?; + if workspaces.is_empty() { + return Err("No workspaces found to export".to_string()); + } + return Ok(workspaces.into_iter().map(|w| w.id).collect()); + } + + if workspace_ids.is_empty() { + return resolve_workspace_id(ctx, None, "export").map(|id| vec![id]); + } + + for workspace_id in &workspace_ids { + ctx.db() + .get_workspace(workspace_id) + .map_err(|e| format!("Failed to get workspace '{workspace_id}': {e}"))?; + } + Ok(workspace_ids) +} + +fn read_import_file(path: &std::path::Path) -> CommandResult { + fs::read_to_string(path).map_err(|err| { + if err.kind() == ErrorKind::InvalidData { + format!( + "Import file must be UTF-8 text; binary files are not supported: {}", + path.display() + ) + } else { + format!("Unable to read import file {}: {err}", path.display()) + } + }) +} + +fn resources_need_current_workspace(resources: &ImportResources) -> bool { + resources.workspaces.iter().any(|w| w.id == "CURRENT_WORKSPACE") + || resources.environments.iter().any(|e| { + e.workspace_id == "CURRENT_WORKSPACE" + || e.parent_id.as_deref() == Some("CURRENT_WORKSPACE") + }) + || resources.folders.iter().any(|f| { + f.workspace_id == "CURRENT_WORKSPACE" + || f.folder_id.as_deref() == Some("CURRENT_WORKSPACE") + }) + || resources.http_requests.iter().any(|r| { + r.workspace_id == "CURRENT_WORKSPACE" + || r.folder_id.as_deref() == Some("CURRENT_WORKSPACE") + }) + || resources.grpc_requests.iter().any(|r| { + r.workspace_id == "CURRENT_WORKSPACE" + || r.folder_id.as_deref() == Some("CURRENT_WORKSPACE") + }) + || resources.websocket_requests.iter().any(|r| { + r.workspace_id == "CURRENT_WORKSPACE" + || r.folder_id.as_deref() == Some("CURRENT_WORKSPACE") + }) +} + +fn format_counts(result: &BatchUpsertResult) -> String { + let names = [ + "workspace", + "environment", + "folder", + "HTTP request", + "gRPC request", + "WebSocket request", + ]; + let counts = [ + (result.workspaces.len(), names[0]), + (result.environments.len(), names[1]), + (result.folders.len(), names[2]), + (result.http_requests.len(), names[3]), + (result.grpc_requests.len(), names[4]), + (result.websocket_requests.len(), names[5]), + ]; + + let non_zero: Vec = counts + .into_iter() + .filter(|(count, _)| *count > 0) + .map(|(count, name)| format!("{count} {name}{}", if count == 1 { "" } else { "s" })) + .collect(); + + if non_zero.is_empty() { "nothing".to_string() } else { non_zero.join(", ") } +} diff --git a/crates-cli/yaak-cli/src/commands/mod.rs b/crates-cli/yaak-cli/src/commands/mod.rs index dc52933a..d19d9c9e 100644 --- a/crates-cli/yaak-cli/src/commands/mod.rs +++ b/crates-cli/yaak-cli/src/commands/mod.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod cookie_jar; pub mod environment; pub mod folder; +pub mod import_export; pub mod plugin; pub mod request; pub mod send; diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index b5a2ad30..eda769d5 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -37,6 +37,23 @@ async fn main() { let exit_code = match command { Commands::Auth(args) => commands::auth::run(args).await, + Commands::Import(args) => { + let mut context = CliContext::new(data_dir.clone(), app_id); + let execution_context = CliExecutionContext { + workspace_id: args.workspace_id.clone(), + ..CliExecutionContext::default() + }; + context.init_plugins(execution_context).await; + let exit_code = commands::import_export::run_import(&context, args).await; + context.shutdown().await; + exit_code + } + Commands::Export(args) => { + let context = CliContext::new(data_dir.clone(), app_id); + let exit_code = commands::import_export::run_export(&context, args); + context.shutdown().await; + exit_code + } Commands::Plugin(args) => match args.command { PluginCommands::Build(args) => commands::plugin::run_build(args).await, PluginCommands::Dev(args) => commands::plugin::run_dev(args).await, diff --git a/crates-cli/yaak-cli/tests/import_export_commands.rs b/crates-cli/yaak-cli/tests/import_export_commands.rs new file mode 100644 index 00000000..2921beb4 --- /dev/null +++ b/crates-cli/yaak-cli/tests/import_export_commands.rs @@ -0,0 +1,162 @@ +mod common; + +use common::{cli_cmd, parse_created_id, query_manager, seed_request}; +use predicates::str::contains; +use serde_json::Value; +use tempfile::TempDir; + +#[test] +fn export_writes_yaak_workspace_file() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + let export_path = temp_dir.path().join("export.json"); + + let create_assert = + cli_cmd(data_dir).args(["workspace", "create", "--name", "Export Me"]).assert().success(); + let workspace_id = parse_created_id(&create_assert.get_output().stdout, "workspace create"); + seed_request(data_dir, &workspace_id, "req_export"); + + cli_cmd(data_dir) + .args([ + "export", + export_path.to_str().expect("export path is utf-8"), + &workspace_id, + ]) + .assert() + .success() + .stdout(contains("Exported 1 workspace(s)")); + + let exported: Value = serde_json::from_str( + &std::fs::read_to_string(export_path).expect("export file should exist"), + ) + .expect("export should be JSON"); + + assert_eq!(exported["yaakSchema"], 4); + assert_eq!(exported["resources"]["workspaces"][0]["id"], workspace_id); + assert_eq!(exported["resources"]["httpRequests"][0]["id"], "req_export"); +} + +#[test] +fn import_reads_yaak_workspace_file() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + let import_path = temp_dir.path().join("import.json"); + + std::fs::write( + &import_path, + r#"{ + "yaakVersion": "test", + "yaakSchema": 4, + "resources": { + "workspaces": [ + { + "model": "workspace", + "id": "wrk_import", + "name": "Imported Workspace" + } + ], + "httpRequests": [ + { + "model": "http_request", + "id": "req_import", + "workspaceId": "wrk_import", + "name": "Imported Request", + "method": "GET", + "url": "https://example.com" + } + ] + } +}"#, + ) + .expect("write import fixture"); + + cli_cmd(data_dir) + .args([ + "import", + import_path.to_str().expect("import path is utf-8"), + ]) + .assert() + .success() + .stdout(contains("Imported 1 workspace, 1 HTTP request")); + + let query_manager = query_manager(data_dir); + let db = query_manager.connect(); + assert_eq!( + db.get_workspace("wrk_import").expect("workspace imported").name, + "Imported Workspace" + ); + assert_eq!( + db.get_http_request("req_import").expect("request imported").url, + "https://example.com" + ); +} + +fn write_postman_environment_fixture(path: &std::path::Path) { + std::fs::write( + path, + r#"{ + "name": "Local", + "_postman_variable_scope": "environment", + "values": [ + { + "key": "token", + "value": "abc123", + "enabled": true + } + ] +}"#, + ) + .expect("write postman environment fixture"); +} + +#[test] +fn import_postman_environment_requires_workspace_id() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + let import_path = temp_dir.path().join("postman-env.json"); + + cli_cmd(data_dir).args(["workspace", "create", "--name", "Env Target"]).assert().success(); + write_postman_environment_fixture(&import_path); + + cli_cmd(data_dir) + .args([ + "import", + import_path.to_str().expect("import path is utf-8"), + ]) + .assert() + .failure() + .stderr(contains("requires a workspace context")) + .stderr(contains("--workspace-id")); +} + +#[test] +fn import_postman_environment_uses_workspace_id() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + let import_path = temp_dir.path().join("postman-env.json"); + + let create_assert = + cli_cmd(data_dir).args(["workspace", "create", "--name", "Env Target"]).assert().success(); + let workspace_id = parse_created_id(&create_assert.get_output().stdout, "workspace create"); + write_postman_environment_fixture(&import_path); + + cli_cmd(data_dir) + .args([ + "import", + import_path.to_str().expect("import path is utf-8"), + "--workspace-id", + &workspace_id, + ]) + .assert() + .success() + .stdout(contains("Imported 1 environment")); + + 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 imported_environment = + environments.iter().find(|e| e.name == "Local").expect("postman environment imported"); + assert_eq!(imported_environment.workspace_id, workspace_id); +} diff --git a/crates-tauri/yaak-app-client/src/error.rs b/crates-tauri/yaak-app-client/src/error.rs index 10659d53..a8a6f446 100644 --- a/crates-tauri/yaak-app-client/src/error.rs +++ b/crates-tauri/yaak-app-client/src/error.rs @@ -38,6 +38,9 @@ pub enum Error { #[error(transparent)] ApiError(#[from] yaak_api::Error), + #[error(transparent)] + YaakError(#[from] yaak::Error), + #[error(transparent)] ClipboardError(#[from] tauri_plugin_clipboard_manager::Error), diff --git a/crates-tauri/yaak-app-client/src/import.rs b/crates-tauri/yaak-app-client/src/import.rs index 53c94a5e..ddb2932e 100644 --- a/crates-tauri/yaak-app-client/src/import.rs +++ b/crates-tauri/yaak-app-client/src/import.rs @@ -1,16 +1,12 @@ use crate::PluginContextExt; use crate::error::{Error, Result}; use crate::models_ext::QueryManagerExt; -use log::info; -use std::collections::BTreeMap; use std::fs::read_to_string; use std::io::ErrorKind; use tauri::{Manager, Runtime, WebviewWindow}; +use yaak::import::{self, ImportDataParams}; use yaak_core::WorkspaceContext; -use yaak_models::models::{ - Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, -}; -use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt}; +use yaak_models::util::BatchUpsertResult; use yaak_plugins::manager::PluginManager; use yaak_tauri_utils::window::WorkspaceWindowTrait; @@ -19,113 +15,24 @@ pub(crate) async fn import_data( file_path: &str, ) -> Result { let plugin_manager = window.state::(); + let query_manager = window.db_manager(); let file = read_import_file(file_path)?; - let file_contents = file.as_str(); - let import_result = plugin_manager.import_data(&window.plugin_context(), file_contents).await?; - - let mut id_map: BTreeMap = BTreeMap::new(); - - // Create WorkspaceContext from window - let ctx = WorkspaceContext { + let plugin_context = window.plugin_context(); + let workspace_context = WorkspaceContext { workspace_id: window.workspace_id(), environment_id: window.environment_id(), cookie_jar_id: window.cookie_jar_id(), request_id: None, }; - let resources = import_result.resources; - - let workspaces: Vec = resources - .workspaces - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v - }) - .collect(); - - let environments: Vec = resources - .environments - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(&ctx, v.workspace_id.as_str(), &mut id_map); - match (v.parent_model.as_str(), v.parent_id.clone().as_deref()) { - ("folder", Some(parent_id)) => { - v.parent_id = Some(maybe_gen_id::(&ctx, &parent_id, &mut id_map)); - } - ("", _) => { - // Fix any empty ones - v.parent_model = "workspace".to_string(); - } - _ => { - // Parent ID only required for the folder case - v.parent_id = None; - } - }; - v - }) - .collect(); - - let folders: Vec = resources - .folders - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(&ctx, v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(&ctx, v.folder_id, &mut id_map); - v - }) - .collect(); - - let http_requests: Vec = resources - .http_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(&ctx, v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(&ctx, v.folder_id, &mut id_map); - v - }) - .collect(); - - let grpc_requests: Vec = resources - .grpc_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(&ctx, v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(&ctx, v.folder_id, &mut id_map); - v - }) - .collect(); - - let websocket_requests: Vec = resources - .websocket_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(&ctx, v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(&ctx, v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(&ctx, v.folder_id, &mut id_map); - v - }) - .collect(); - - info!("Importing data"); - - let upserted = window.with_tx(|tx| { - tx.batch_upsert( - workspaces, - environments, - folders, - http_requests, - grpc_requests, - websocket_requests, - &UpdateSource::Import, - ) - })?; - - Ok(upserted) + Ok(import::import_data(ImportDataParams { + query_manager: &query_manager, + plugin_manager: &plugin_manager, + plugin_context: &plugin_context, + workspace_context, + contents: &file, + }) + .await?) } fn read_import_file(file_path: &str) -> Result { diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index 1721f963..bf6d02ce 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -14,8 +14,7 @@ use error::Result as YaakResult; use eventsource_client::{EventParser, SSE}; use log::{debug, error, info, warn}; use std::collections::HashMap; -use std::fs::File; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -31,6 +30,7 @@ use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tokio::sync::Mutex; use tokio::task::block_in_place; use tokio::time; +use yaak::export::{self, ExportDataParams}; use yaak_common::command::new_checked_command; use yaak_crypto::manager::EncryptionManager; use yaak_grpc::manager::{GrpcConfig, GrpcHandle}; @@ -41,7 +41,7 @@ use yaak_models::models::{ GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Workspace, WorkspaceMeta, }; -use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; +use yaak_models::util::{BatchUpsertResult, UpdateSource}; use yaak_plugins::events::{ CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, @@ -1384,24 +1384,14 @@ async fn cmd_export_data( workspace_ids: Vec<&str>, include_private_environments: bool, ) -> YaakResult<()> { - let db = app_handle.db(); let version = app_handle.package_info().version.to_string(); - let export_data = - get_workspace_export_resources(&db, &version, workspace_ids, include_private_environments)?; - let f = File::options() - .create(true) - .truncate(true) - .write(true) - .open(export_path) - .expect("Unable to create file"); - - serde_json::to_writer_pretty(&f, &export_data) - .map_err(|e| GenericError(e.to_string())) - .expect("Failed to write"); - - f.sync_all().expect("Failed to sync"); - - Ok(()) + Ok(export::export_data(ExportDataParams { + query_manager: &app_handle.db_manager(), + yaak_version: &version, + export_path: Path::new(export_path), + workspace_ids, + include_private_environments, + })?) } #[tauri::command] diff --git a/crates/yaak/Cargo.toml b/crates/yaak/Cargo.toml index 3c32ecf2..d24bc012 100644 --- a/crates/yaak/Cargo.toml +++ b/crates/yaak/Cargo.toml @@ -12,6 +12,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["sync", "rt"] } yaak-http = { workspace = true } +yaak-core = { workspace = true } yaak-crypto = { workspace = true } yaak-models = { workspace = true } yaak-plugins = { workspace = true } diff --git a/crates/yaak/src/error.rs b/crates/yaak/src/error.rs index 322c78d8..7b14c7a8 100644 --- a/crates/yaak/src/error.rs +++ b/crates/yaak/src/error.rs @@ -4,6 +4,18 @@ use thiserror::Error; pub enum Error { #[error(transparent)] Send(#[from] crate::send::SendHttpRequestError), + + #[error(transparent)] + Model(#[from] yaak_models::error::Error), + + #[error(transparent)] + Plugin(#[from] yaak_plugins::error::Error), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), } pub type Result = std::result::Result; diff --git a/crates/yaak/src/export.rs b/crates/yaak/src/export.rs new file mode 100644 index 00000000..473f9d56 --- /dev/null +++ b/crates/yaak/src/export.rs @@ -0,0 +1,29 @@ +use crate::Result; +use std::fs::File; +use std::path::Path; +use yaak_models::query_manager::QueryManager; +use yaak_models::util::get_workspace_export_resources; + +pub struct ExportDataParams<'a> { + pub query_manager: &'a QueryManager, + pub yaak_version: &'a str, + pub export_path: &'a Path, + pub workspace_ids: Vec<&'a str>, + pub include_private_environments: bool, +} + +pub fn export_data(params: ExportDataParams<'_>) -> Result<()> { + let db = params.query_manager.connect(); + let export_data = get_workspace_export_resources( + &db, + params.yaak_version, + params.workspace_ids, + params.include_private_environments, + )?; + + let file = File::options().create(true).truncate(true).write(true).open(params.export_path)?; + serde_json::to_writer_pretty(&file, &export_data)?; + file.sync_all()?; + + Ok(()) +} diff --git a/crates/yaak/src/import.rs b/crates/yaak/src/import.rs new file mode 100644 index 00000000..5ba7aa3e --- /dev/null +++ b/crates/yaak/src/import.rs @@ -0,0 +1,129 @@ +use crate::Result; +use log::info; +use std::collections::BTreeMap; +use yaak_core::WorkspaceContext; +use yaak_models::models::{ + Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, +}; +use yaak_models::query_manager::QueryManager; +use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt}; +use yaak_plugins::events::{ImportResources, PluginContext}; +use yaak_plugins::manager::PluginManager; + +pub struct ImportDataParams<'a> { + pub query_manager: &'a QueryManager, + pub plugin_manager: &'a PluginManager, + pub plugin_context: &'a PluginContext, + pub workspace_context: WorkspaceContext, + pub contents: &'a str, +} + +pub async fn import_data(params: ImportDataParams<'_>) -> Result { + let import_result = + params.plugin_manager.import_data(params.plugin_context, params.contents).await?; + + import_resources(params.query_manager, params.workspace_context, import_result.resources) +} + +pub fn import_resources( + query_manager: &QueryManager, + workspace_context: WorkspaceContext, + resources: ImportResources, +) -> Result { + let mut id_map: BTreeMap = BTreeMap::new(); + + let workspaces: Vec = resources + .workspaces + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v + }) + .collect(); + + let environments: Vec = resources + .environments + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v.workspace_id = + maybe_gen_id::(&workspace_context, v.workspace_id.as_str(), &mut id_map); + match (v.parent_model.as_str(), v.parent_id.clone().as_deref()) { + ("folder", Some(parent_id)) => { + v.parent_id = + Some(maybe_gen_id::(&workspace_context, parent_id, &mut id_map)); + } + ("", _) => { + v.parent_model = "workspace".to_string(); + } + _ => { + v.parent_id = None; + } + }; + v + }) + .collect(); + + let folders: Vec = resources + .folders + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v.workspace_id = + maybe_gen_id::(&workspace_context, v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(&workspace_context, v.folder_id, &mut id_map); + v + }) + .collect(); + + let http_requests: Vec = resources + .http_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v.workspace_id = + maybe_gen_id::(&workspace_context, v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(&workspace_context, v.folder_id, &mut id_map); + v + }) + .collect(); + + let grpc_requests: Vec = resources + .grpc_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v.workspace_id = + maybe_gen_id::(&workspace_context, v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(&workspace_context, v.folder_id, &mut id_map); + v + }) + .collect(); + + let websocket_requests: Vec = resources + .websocket_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(&workspace_context, v.id.as_str(), &mut id_map); + v.workspace_id = + maybe_gen_id::(&workspace_context, v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(&workspace_context, v.folder_id, &mut id_map); + v + }) + .collect(); + + info!("Importing data"); + + query_manager.with_tx(|tx| { + tx.batch_upsert( + workspaces, + environments, + folders, + http_requests, + grpc_requests, + websocket_requests, + &UpdateSource::Import, + ) + .map_err(crate::Error::from) + }) +} diff --git a/crates/yaak/src/lib.rs b/crates/yaak/src/lib.rs index 8c79eacd..7bc790e8 100644 --- a/crates/yaak/src/lib.rs +++ b/crates/yaak/src/lib.rs @@ -1,4 +1,6 @@ pub mod error; +pub mod export; +pub mod import; pub mod plugin_events; pub mod render; pub mod send;