From 8fe50959b9094930cd58571e5e80eb9a3a06f9c0 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 22 Sep 2025 11:15:32 -0700 Subject: [PATCH] Add migrate for base environment to sync logic --- plugins/importer-yaak/src/index.ts | 17 ++- src-tauri/Cargo.lock | 32 ++++- .../yaak-models/src/queries/environments.rs | 4 +- src-tauri/yaak-sync/Cargo.toml | 1 + src-tauri/yaak-sync/src/models.rs | 133 +++++++++++++++++- src-web/components/EnvironmentEditor.tsx | 2 +- src-web/components/core/Editor/Editor.tsx | 5 +- src-web/hooks/useEnvironmentVariables.ts | 2 +- src-web/hooks/useEnvironmentsBreakdown.ts | 4 +- 9 files changed, 182 insertions(+), 18 deletions(-) diff --git a/plugins/importer-yaak/src/index.ts b/plugins/importer-yaak/src/index.ts index 66b0333b..61fd6e38 100644 --- a/plugins/importer-yaak/src/index.ts +++ b/plugins/importer-yaak/src/index.ts @@ -66,7 +66,22 @@ export function migrateImport(contents: string) { } } - return { resources: parsed.resources }; // Should already be in the correct format + // Migrate v4 to v5 + for (const environment of parsed.resources.environments ?? []) { + if ('base' in environment && environment.base) { + environment.parentId = environment.workspaceId; + environment.parentType = 'workspace'; + delete environment.environmentId; + } else if ('base' in environment && !environment.base) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseEnvironment = parsed.resources.environments.find((e: any) => e.base); + environment.parentId = baseEnvironment?.id ?? null; + environment.parentType = 'environment'; + delete environment.environmentId; + } + } + + return { resources: parsed.resources }; } function isJSObject(obj: unknown) { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 352c1cc3..1b87afce 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5163,10 +5163,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ + "serde_core", "serde_derive", ] @@ -5203,10 +5204,19 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -5236,6 +5246,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -8162,6 +8183,7 @@ dependencies = [ "notify", "serde", "serde_json", + "serde_path_to_error", "serde_yaml", "sha1", "tauri", diff --git a/src-tauri/yaak-models/src/queries/environments.rs b/src-tauri/yaak-models/src/queries/environments.rs index 92418f7a..599753e8 100644 --- a/src-tauri/yaak-models/src/queries/environments.rs +++ b/src-tauri/yaak-models/src/queries/environments.rs @@ -26,7 +26,7 @@ impl<'a> DbContext<'a> { let environments = self.list_environments_ensure_base(workspace_id)?; let base_environments = environments .into_iter() - .filter(|e| e.parent_id.is_none()) + .filter(|e| e.parent_model == "workspace") .collect::>(); if base_environments.len() > 1 { @@ -44,7 +44,7 @@ impl<'a> DbContext<'a> { let mut environments = self.find_many::(EnvironmentIden::WorkspaceId, workspace_id, None)?; - let base_environment = environments.iter().find(|e| e.parent_id.is_none()); + let base_environment = environments.iter().find(|e| e.parent_model == "workspace"); if let None = base_environment { let e = self.upsert_environment( diff --git a/src-tauri/yaak-sync/Cargo.toml b/src-tauri/yaak-sync/Cargo.toml index a843fc0a..5b50fd8b 100644 --- a/src-tauri/yaak-sync/Cargo.toml +++ b/src-tauri/yaak-sync/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "sync", "macros"] } ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] } yaak-models = { workspace = true } +serde_path_to_error = "0.1.20" [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } diff --git a/src-tauri/yaak-sync/src/models.rs b/src-tauri/yaak-sync/src/models.rs index e7e8e074..1d0ccad5 100644 --- a/src-tauri/yaak-sync/src/models.rs +++ b/src-tauri/yaak-sync/src/models.rs @@ -2,7 +2,8 @@ use crate::error::Error::UnknownModel; use crate::error::Result; use chrono::NaiveDateTime; use log::warn; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_yaml::{Mapping, Value}; use sha1::{Digest, Sha1}; use std::fs; use std::path::Path; @@ -11,7 +12,7 @@ use yaak_models::models::{ AnyModel, Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, }; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "snake_case", tag = "type")] #[ts(export, export_to = "gen_models.ts")] pub enum SyncModel { @@ -23,6 +24,78 @@ pub enum SyncModel { WebsocketRequest(WebsocketRequest), } +impl<'de> Deserialize<'de> for SyncModel { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + use serde_path_to_error as spte; + let mut v = Value::deserialize(deserializer)?; + let model = match v.get("model") { + Some(Value::String(model)) => model.clone(), + _ => "".to_string(), + }; + let model = model.as_str(); + + let obj = v + .as_mapping_mut() + .ok_or_else(|| serde::de::Error::custom("expected object for SyncModel"))?; + + // Dispatch to CHILD types (no recursion) + match model { + "workspace" => { + let x: Workspace = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::Workspace(x)) + } + "environment" => { + migrate_environment(obj); + let x: Environment = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::Environment(x)) + } + "folder" => { + let x: Folder = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::Folder(x)) + } + "http_request" => { + let x: HttpRequest = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::HttpRequest(x)) + } + "grpc_request" => { + let x: GrpcRequest = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::GrpcRequest(x)) + } + "websocket_request" => { + let x: WebsocketRequest = spte::deserialize(v).map_err(serde::de::Error::custom)?; + Ok(SyncModel::WebsocketRequest(x)) + } + other => Err(serde::de::Error::unknown_variant( + other, + &[ + "workspace", + "environment", + "folder", + "http_request", + "grpc_request", + "websocket_request", + ], + )), + } + } +} + +fn migrate_environment(obj: &mut Mapping) { + match obj.get("base") { + Some(Value::Bool(base)) => { + if *base { + obj.insert("parentModel".into(), "workspace".into()); + } else { + obj.insert("parentModel".into(), "environment".into()); + } + } + _ => {} + } +} + impl SyncModel { pub fn from_bytes(content: Vec, file_path: &Path) -> Result> { let mut hasher = Sha1::new(); @@ -145,3 +218,59 @@ impl TryFrom for SyncModel { Ok(m) } } + +#[cfg(test)] +mod placeholder_tests { + use crate::error::Result; + use crate::models::SyncModel; + + #[test] + fn deserializes_environment_via_syncmodel_with_fixups() -> Result<()> { + let raw = r#" +type: environment +model: environment +id: ev_fAUS49FUN2 +workspaceId: wk_kfSI3JDHd7 +createdAt: 2025-01-11T17:02:58.012792 +updatedAt: 2025-07-23T20:00:46.049649 +name: Global Variables +public: true +base: true +variables: [] +color: null +"#; + + let m: SyncModel = serde_yaml::from_str(raw)?; + match m { + SyncModel::Environment(env) => { + assert_eq!(env.parent_model, "workspace".to_string()); + assert_eq!(env.parent_id, None); + } + _ => panic!("expected base environment"), + } + + let raw = r#" +type: environment +model: environment +id: ev_fAUS49FUN2 +workspaceId: wk_kfSI3JDHd7 +createdAt: 2025-01-11T17:02:58.012792 +updatedAt: 2025-07-23T20:00:46.049649 +name: Global Variables +public: true +base: false +variables: [] +color: null +"#; + let m: SyncModel = serde_yaml::from_str(raw)?; + match m { + SyncModel::Environment(env) => { + assert_eq!(env.parent_model, "environment".to_string()); + assert_eq!(env.parent_id, None); + } + _ => panic!("expected sub environment"), + } + + Ok(()) + } +} diff --git a/src-web/components/EnvironmentEditor.tsx b/src-web/components/EnvironmentEditor.tsx index deb90251..2648be49 100644 --- a/src-web/components/EnvironmentEditor.tsx +++ b/src-web/components/EnvironmentEditor.tsx @@ -136,7 +136,7 @@ export function EnvironmentEditor({ { label: 'Encrypt Variables', onClick: () => encryptEnvironment(environment), - color: 'primary', + color: 'success', }, ]} > diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index b57a5d2e..40bc9396 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -27,7 +27,6 @@ import { useMemo, useRef, } from 'react'; -import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment'; import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables'; import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables'; import { useRequestEditor } from '../../../hooks/useRequestEditor'; @@ -138,9 +137,7 @@ export const Editor = forwardRef(function E ) { const settings = useAtomValue(settingsAtom); - const activeEnvironmentId = useAtomValue(activeEnvironmentIdAtom); - const environmentId = forcedEnvironmentId ?? activeEnvironmentId ?? null; - const allEnvironmentVariables = useEnvironmentVariables(environmentId); + const allEnvironmentVariables = useEnvironmentVariables(forcedEnvironmentId ?? null); const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables; const useTemplating = !!(autocompleteFunctions || autocompleteVariables || autocomplete); diff --git a/src-web/hooks/useEnvironmentVariables.ts b/src-web/hooks/useEnvironmentVariables.ts index 334db625..9be8ddc1 100644 --- a/src-web/hooks/useEnvironmentVariables.ts +++ b/src-web/hooks/useEnvironmentVariables.ts @@ -26,7 +26,7 @@ export function useEnvironmentVariables(targetEnvironmentId: string | null) { // Folder environments also can auto-complete from the active environment const activeEnvironmentVariables = - targetEnvironment != null && isFolderEnvironment(targetEnvironment) + targetEnvironment == null || isFolderEnvironment(targetEnvironment) ? wrapVariables(activeEnvironment) : []; diff --git a/src-web/hooks/useEnvironmentsBreakdown.ts b/src-web/hooks/useEnvironmentsBreakdown.ts index ce73d3ca..5480ece4 100644 --- a/src-web/hooks/useEnvironmentsBreakdown.ts +++ b/src-web/hooks/useEnvironmentsBreakdown.ts @@ -5,9 +5,9 @@ import { useMemo } from 'react'; export function useEnvironmentsBreakdown() { const allEnvironments = useAtomValue(environmentsAtom); return useMemo(() => { - const baseEnvironments = allEnvironments.filter((e) => e.parentId == null) ?? []; + const baseEnvironments = allEnvironments.filter((e) => e.parentModel == 'workspace') ?? []; const subEnvironments = - allEnvironments.filter((e) => e.parentModel === 'environment' && e.parentId != null) ?? []; + allEnvironments.filter((e) => e.parentModel === 'environment') ?? []; const folderEnvironments = allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? [];