mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-30 01:51:37 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09adcda2d9 | |||
| 18b983bfe5 | |||
| 9ffd8d4810 | |||
| 55d0066efd |
Generated
+2
@@ -10052,6 +10052,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"yaak-core",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
@@ -10182,6 +10183,7 @@ dependencies = [
|
|||||||
"webbrowser",
|
"webbrowser",
|
||||||
"yaak",
|
"yaak",
|
||||||
"yaak-api",
|
"yaak-api",
|
||||||
|
"yaak-core",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
|
|||||||
return key !== nextCookieKey;
|
return key !== nextCookieKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
patchModel(cookieJar, { cookies: [...nextCookies, nextCookie] });
|
void patchModel(cookieJar, { cookies: [...nextCookies, nextCookie] });
|
||||||
setSelectedCookieKey(nextCookieKey);
|
setSelectedCookieKey(nextCookieKey);
|
||||||
setEditingCookieKey(null);
|
setEditingCookieKey(null);
|
||||||
setDraftCookie(null);
|
setDraftCookie(null);
|
||||||
@@ -210,7 +210,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
|
|||||||
setEditingCookieKey(null);
|
setEditingCookieKey(null);
|
||||||
setDraftCookie(null);
|
setDraftCookie(null);
|
||||||
setDraftExpiresInput("");
|
setDraftExpiresInput("");
|
||||||
patchModel(cookieJar, { cookies: [] });
|
void patchModel(cookieJar, { cookies: [] });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
@@ -276,7 +276,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
|
|||||||
setDraftCookie(null);
|
setDraftCookie(null);
|
||||||
setDraftExpiresInput("");
|
setDraftExpiresInput("");
|
||||||
}
|
}
|
||||||
patchModel(cookieJar, {
|
void patchModel(cookieJar, {
|
||||||
cookies: cookieJar.cookies.filter(
|
cookies: cookieJar.cookies.filter(
|
||||||
(c2: Cookie) => cookieKey(c2) !== key,
|
(c2: Cookie) => cookieKey(c2) !== key,
|
||||||
),
|
),
|
||||||
@@ -570,6 +570,8 @@ function CookieTextInput({
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
className={cookieInputClassName}
|
className={cookieInputClassName}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
@@ -585,6 +587,8 @@ function CookieTextInput({
|
|||||||
function CookieTextarea({ onChange, value }: { onChange: (value: string) => void; value: string }) {
|
function CookieTextarea({ onChange, value }: { onChange: (value: string) => void; value: string }) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")}
|
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { patchModel, workspaceMetasAtom, workspacesAtom } from "@yaakapp-internal/models";
|
import { patchModel, workspaceMetasAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
import { Banner, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useAuthTab } from "../hooks/useAuthTab";
|
import { useAuthTab } from "../hooks/useAuthTab";
|
||||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||||
|
|||||||
@@ -580,6 +580,10 @@ function getExtensions({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
...baseExtensions, // Must be first
|
...baseExtensions, // Must be first
|
||||||
|
EditorView.contentAttributes.of({
|
||||||
|
autocapitalize: "off",
|
||||||
|
autocorrect: "off",
|
||||||
|
}),
|
||||||
EditorView.domEventHandlers({
|
EditorView.domEventHandlers({
|
||||||
focus: () => {
|
focus: () => {
|
||||||
onFocus.current?.();
|
onFocus.current?.();
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export function KeyValueRow({
|
|||||||
const textToCopy =
|
const textToCopy =
|
||||||
copyText ??
|
copyText ??
|
||||||
(typeof children === "string" || typeof children === "number" ? `${children}` : null);
|
(typeof children === "string" || typeof children === "number" ? `${children}` : null);
|
||||||
|
const copyTitle =
|
||||||
|
typeof label === "string" || typeof label === "number" ? `Copy ${label}` : "Copy value";
|
||||||
const resolvedRightSlot =
|
const resolvedRightSlot =
|
||||||
rightSlot ??
|
rightSlot ??
|
||||||
(enableCopy && textToCopy != null ? (
|
(enableCopy && textToCopy != null ? (
|
||||||
@@ -62,7 +64,7 @@ export function KeyValueRow({
|
|||||||
text={textToCopy}
|
text={textToCopy}
|
||||||
className="text-text-subtle"
|
className="text-text-subtle"
|
||||||
size="2xs"
|
size="2xs"
|
||||||
title={`Copy ${label}`}
|
title={copyTitle}
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
/>
|
/>
|
||||||
) : null);
|
) : null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useGitFileDiffForCommit, useGitLog, useGitMutations } from "@yaakapp-internal/git";
|
import { useGitFileDiffForCommit, useGitLog, useGitMutations } from "@yaakapp-internal/git";
|
||||||
import type { GitCommit } from "@yaakapp-internal/git";
|
import type { GitCommit } from "@yaakapp-internal/git";
|
||||||
import { InlineCode, SplitLayout } from "@yaakapp-internal/ui";
|
import { SplitLayout } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
WebsocketRequest,
|
WebsocketRequest,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from "@yaakapp-internal/models";
|
} from "@yaakapp-internal/models";
|
||||||
import { Banner, HStack, Icon, IconButton, InlineCode, SplitLayout } from "@yaakapp-internal/ui";
|
import { Banner, HStack, Icon, InlineCode, SplitLayout } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { modelToYaml } from "../../lib/diffYaml";
|
import { modelToYaml } from "../../lib/diffYaml";
|
||||||
|
|||||||
@@ -1,40 +1,32 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { getModel } from "@yaakapp-internal/models";
|
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { invokeCmd } from "../lib/tauri";
|
||||||
import { getActiveCookieJar } from "./useActiveCookieJar";
|
import { getActiveCookieJar } from "./useActiveCookieJar";
|
||||||
import { getActiveEnvironment } from "./useActiveEnvironment";
|
import { getActiveEnvironment } from "./useActiveEnvironment";
|
||||||
import { createFastMutation, useFastMutation } from "./useFastMutation";
|
import { createFastMutation, useFastMutation } from "./useFastMutation";
|
||||||
|
|
||||||
|
async function sendAnyHttpRequestById(id: string | null): Promise<HttpResponse | null> {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushAllModelWrites();
|
||||||
|
|
||||||
|
return invokeCmd("cmd_send_http_request", {
|
||||||
|
requestId: id,
|
||||||
|
environmentId: getActiveEnvironment()?.id,
|
||||||
|
cookieJarId: getActiveCookieJar()?.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useSendAnyHttpRequest() {
|
export function useSendAnyHttpRequest() {
|
||||||
return useFastMutation<HttpResponse | null, string, string | null>({
|
return useFastMutation<HttpResponse | null, string, string | null>({
|
||||||
mutationKey: ["send_any_request"],
|
mutationKey: ["send_any_request"],
|
||||||
mutationFn: async (id) => {
|
mutationFn: sendAnyHttpRequestById,
|
||||||
const request = getModel("http_request", id ?? "n/a");
|
|
||||||
if (request == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return invokeCmd("cmd_send_http_request", {
|
|
||||||
request,
|
|
||||||
environmentId: getActiveEnvironment()?.id,
|
|
||||||
cookieJarId: getActiveCookieJar()?.id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendAnyHttpRequest = createFastMutation<HttpResponse | null, string, string | null>({
|
export const sendAnyHttpRequest = createFastMutation<HttpResponse | null, string, string | null>({
|
||||||
mutationKey: ["send_any_request"],
|
mutationKey: ["send_any_request"],
|
||||||
mutationFn: async (id) => {
|
mutationFn: sendAnyHttpRequestById,
|
||||||
const request = getModel("http_request", id ?? "n/a");
|
|
||||||
if (request == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return invokeCmd("cmd_send_http_request", {
|
|
||||||
request,
|
|
||||||
environmentId: getActiveEnvironment()?.id,
|
|
||||||
cookieJarId: getActiveCookieJar()?.id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ webbrowser = "1"
|
|||||||
zip = "4"
|
zip = "4"
|
||||||
yaak = { workspace = true }
|
yaak = { workspace = true }
|
||||||
yaak-api = { workspace = true }
|
yaak-api = { workspace = true }
|
||||||
|
yaak-core = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ pub enum Commands {
|
|||||||
/// Authentication commands
|
/// Authentication commands
|
||||||
Auth(AuthArgs),
|
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 development and publishing commands
|
||||||
Plugin(PluginArgs),
|
Plugin(PluginArgs),
|
||||||
|
|
||||||
@@ -92,6 +98,34 @@ pub struct SendArgs {
|
|||||||
pub fail_fast: bool,
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Args)]
|
||||||
#[command(disable_help_subcommand = true)]
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct CookieJarArgs {
|
pub struct CookieJarArgs {
|
||||||
@@ -447,6 +481,10 @@ pub enum PluginCommands {
|
|||||||
/// Install a plugin from a local directory or from the registry
|
/// Install a plugin from a local directory or from the registry
|
||||||
Install(InstallPluginArgs),
|
Install(InstallPluginArgs),
|
||||||
|
|
||||||
|
/// Generate plugin metadata for the registry
|
||||||
|
#[command(hide = true)]
|
||||||
|
Metadata(PluginPathArg),
|
||||||
|
|
||||||
/// Publish a Yaak plugin version to the plugin registry
|
/// Publish a Yaak plugin version to the plugin registry
|
||||||
Publish(PluginPathArg),
|
Publish(PluginPathArg),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<T = ()> = std::result::Result<T, String>;
|
||||||
|
|
||||||
|
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<BatchUpsertResult> {
|
||||||
|
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 <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<usize> {
|
||||||
|
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<String>,
|
||||||
|
all: bool,
|
||||||
|
) -> CommandResult<Vec<String>> {
|
||||||
|
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<String> {
|
||||||
|
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<String> = 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(", ") }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod auth;
|
|||||||
pub mod cookie_jar;
|
pub mod cookie_jar;
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub mod folder;
|
pub mod folder;
|
||||||
|
pub mod import_export;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod send;
|
pub mod send;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use std::collections::HashSet;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, IsTerminal, Read, Write};
|
use std::io::{self, IsTerminal, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
@@ -27,6 +28,11 @@ use zip::write::SimpleFileOptions;
|
|||||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||||
|
|
||||||
const KEYRING_USER: &str = "yaak";
|
const KEYRING_USER: &str = "yaak";
|
||||||
|
const METADATA_NODE_BIN: &str = "node";
|
||||||
|
const PLUGIN_RUNTIME_NODE_VERSION: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../packages/plugin-runtime/.node-version"
|
||||||
|
));
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum Environment {
|
enum Environment {
|
||||||
@@ -103,6 +109,16 @@ pub async fn run_publish(args: PluginPathArg) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_metadata(args: PluginPathArg) -> i32 {
|
||||||
|
match metadata(args) {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(error) => {
|
||||||
|
ui::error(&error);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn build(args: PluginPathArg) -> CommandResult {
|
async fn build(args: PluginPathArg) -> CommandResult {
|
||||||
let plugin_dir = resolve_plugin_dir(args.path)?;
|
let plugin_dir = resolve_plugin_dir(args.path)?;
|
||||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
ensure_plugin_build_inputs(&plugin_dir)?;
|
||||||
@@ -112,10 +128,21 @@ async fn build(args: PluginPathArg) -> CommandResult {
|
|||||||
for warning in warnings {
|
for warning in warnings {
|
||||||
ui::warning(&warning);
|
ui::warning(&warning);
|
||||||
}
|
}
|
||||||
|
generate_plugin_metadata(&plugin_dir)?;
|
||||||
ui::success(&format!("Built plugin bundle at {}", plugin_dir.join("build/index.js").display()));
|
ui::success(&format!("Built plugin bundle at {}", plugin_dir.join("build/index.js").display()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn metadata(args: PluginPathArg) -> CommandResult {
|
||||||
|
let plugin_dir = resolve_plugin_dir(args.path)?;
|
||||||
|
generate_plugin_metadata(&plugin_dir)?;
|
||||||
|
ui::success(&format!(
|
||||||
|
"Generated plugin metadata at {}",
|
||||||
|
plugin_dir.join("build/metadata.json").display()
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn dev(args: PluginPathArg) -> CommandResult {
|
async fn dev(args: PluginPathArg) -> CommandResult {
|
||||||
let plugin_dir = resolve_plugin_dir(args.path)?;
|
let plugin_dir = resolve_plugin_dir(args.path)?;
|
||||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
ensure_plugin_build_inputs(&plugin_dir)?;
|
||||||
@@ -153,7 +180,15 @@ async fn dev(args: PluginPathArg) -> CommandResult {
|
|||||||
});
|
});
|
||||||
ui::info(&format!("Rebuilding plugin {display_path}"));
|
ui::info(&format!("Rebuilding plugin {display_path}"));
|
||||||
}
|
}
|
||||||
WatcherEvent::Event(BundleEvent::BundleEnd(_)) => {}
|
WatcherEvent::Event(BundleEvent::BundleEnd(_)) => {
|
||||||
|
match generate_plugin_metadata(&watch_root) {
|
||||||
|
Ok(()) => ui::success(&format!(
|
||||||
|
"Generated plugin metadata at {}",
|
||||||
|
watch_root.join("build/metadata.json").display()
|
||||||
|
)),
|
||||||
|
Err(error) => ui::error(&error),
|
||||||
|
}
|
||||||
|
}
|
||||||
WatcherEvent::Event(BundleEvent::Error(event)) => {
|
WatcherEvent::Event(BundleEvent::Error(event)) => {
|
||||||
if event.error.diagnostics.is_empty() {
|
if event.error.diagnostics.is_empty() {
|
||||||
ui::error("Plugin build failed");
|
ui::error("Plugin build failed");
|
||||||
@@ -228,6 +263,7 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
for warning in warnings {
|
for warning in warnings {
|
||||||
ui::warning(&warning);
|
ui::warning(&warning);
|
||||||
}
|
}
|
||||||
|
generate_plugin_metadata(&plugin_dir)?;
|
||||||
|
|
||||||
ui::info("Archiving plugin");
|
ui::info("Archiving plugin");
|
||||||
let archive = create_publish_archive(&plugin_dir)?;
|
let archive = create_publish_archive(&plugin_dir)?;
|
||||||
@@ -379,6 +415,79 @@ async fn build_plugin_bundle(plugin_dir: &Path) -> CommandResult<Vec<String>> {
|
|||||||
Ok(output.warnings.into_iter().map(|w| w.to_string()).collect())
|
Ok(output.warnings.into_iter().map(|w| w.to_string()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_plugin_metadata(plugin_dir: &Path) -> CommandResult {
|
||||||
|
let entry_path = plugin_dir.join("build/index.js");
|
||||||
|
if !entry_path.is_file() {
|
||||||
|
return Err("build/index.js does not exist. Run `yaak plugin build` first.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_metadata_node_version()?;
|
||||||
|
|
||||||
|
let metadata_path = plugin_dir.join("build/metadata.json");
|
||||||
|
let output = Command::new(METADATA_NODE_BIN)
|
||||||
|
.arg("-e")
|
||||||
|
.arg(METADATA_SCRIPT)
|
||||||
|
.arg(entry_path.canonicalize().map_err(|e| {
|
||||||
|
format!("Failed to resolve plugin entrypoint {}: {e}", entry_path.display())
|
||||||
|
})?)
|
||||||
|
.arg(&metadata_path)
|
||||||
|
.current_dir(plugin_dir)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to run Node.js to generate plugin metadata: {e}"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let message = if stderr.is_empty() {
|
||||||
|
format!("Node.js exited with status {}", output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
};
|
||||||
|
return Err(format!("Failed to generate plugin metadata: {message}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_metadata_node_version() -> CommandResult {
|
||||||
|
let minimum_major = PLUGIN_RUNTIME_NODE_VERSION
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('v')
|
||||||
|
.split('.')
|
||||||
|
.next()
|
||||||
|
.and_then(|part| part.parse::<u32>().ok())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Invalid plugin runtime Node.js version {:?} in packages/plugin-runtime/.node-version",
|
||||||
|
PLUGIN_RUNTIME_NODE_VERSION.trim()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let output = Command::new(METADATA_NODE_BIN)
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Node.js {minimum_major} or newer is required: {e}"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"`{METADATA_NODE_BIN} --version` failed with status {}",
|
||||||
|
output.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
let major = version
|
||||||
|
.trim_start_matches('v')
|
||||||
|
.split('.')
|
||||||
|
.next()
|
||||||
|
.and_then(|part| part.parse::<u32>().ok())
|
||||||
|
.ok_or_else(|| format!("Could not parse Node.js version {version:?}"))?;
|
||||||
|
|
||||||
|
if major >= minimum_major {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("Node.js {minimum_major} or newer is required. Found {version}."))
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_build_output_dir(plugin_dir: &Path) -> CommandResult {
|
fn prepare_build_output_dir(plugin_dir: &Path) -> CommandResult {
|
||||||
let build_dir = plugin_dir.join("build");
|
let build_dir = plugin_dir.join("build");
|
||||||
if build_dir.exists() {
|
if build_dir.exists() {
|
||||||
@@ -578,6 +687,11 @@ const TEMPLATE_PACKAGE_JSON: &str = r#"{
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
const METADATA_SCRIPT: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../packages/plugin-runtime/src/metadata.ts"
|
||||||
|
));
|
||||||
|
|
||||||
const TEMPLATE_TSCONFIG: &str = r#"{
|
const TEMPLATE_TSCONFIG: &str = r#"{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
@@ -636,7 +750,8 @@ describe("Example Plugin", () => {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::create_publish_archive;
|
use super::{create_publish_archive, generate_plugin_metadata};
|
||||||
|
use serde_json::Value;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -659,6 +774,7 @@ mod tests {
|
|||||||
.expect("write src/index.ts");
|
.expect("write src/index.ts");
|
||||||
fs::write(root.join("build/index.js"), "exports.plugin = {};\n")
|
fs::write(root.join("build/index.js"), "exports.plugin = {};\n")
|
||||||
.expect("write build/index.js");
|
.expect("write build/index.js");
|
||||||
|
fs::write(root.join("build/metadata.json"), "{}\n").expect("write build/metadata.json");
|
||||||
fs::write(root.join("ignored/secret.txt"), "do-not-ship").expect("write ignored file");
|
fs::write(root.join("ignored/secret.txt"), "do-not-ship").expect("write ignored file");
|
||||||
|
|
||||||
let archive = create_publish_archive(root).expect("create archive");
|
let archive = create_publish_archive(root).expect("create archive");
|
||||||
@@ -673,8 +789,74 @@ mod tests {
|
|||||||
assert!(names.contains("README.md"));
|
assert!(names.contains("README.md"));
|
||||||
assert!(names.contains("package.json"));
|
assert!(names.contains("package.json"));
|
||||||
assert!(names.contains("package-lock.json"));
|
assert!(names.contains("package-lock.json"));
|
||||||
|
assert!(names.contains("build/metadata.json"));
|
||||||
assert!(names.contains("src/index.ts"));
|
assert!(names.contains("src/index.ts"));
|
||||||
assert!(names.contains("build/index.js"));
|
assert!(names.contains("build/index.js"));
|
||||||
assert!(!names.contains("ignored/secret.txt"));
|
assert!(!names.contains("ignored/secret.txt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_plugin_metadata_detects_api_types() {
|
||||||
|
let dir = TempDir::new().expect("temp dir");
|
||||||
|
let root = dir.path();
|
||||||
|
fs::create_dir_all(root.join("build")).expect("create build");
|
||||||
|
fs::write(
|
||||||
|
root.join("build/index.js"),
|
||||||
|
r##"
|
||||||
|
exports.plugin = {
|
||||||
|
themes: [{
|
||||||
|
id: "midnight",
|
||||||
|
label: "Midnight",
|
||||||
|
dark: true,
|
||||||
|
base: { surface: "#000000", text: "#ffffff" },
|
||||||
|
}],
|
||||||
|
templateFunctions: [{
|
||||||
|
name: "signature",
|
||||||
|
description: "Create a signature",
|
||||||
|
args: [{ type: "text", name: "secret", dynamic() {} }],
|
||||||
|
onRender() {},
|
||||||
|
}],
|
||||||
|
workspaceActions: [{
|
||||||
|
label: "Sync workspace",
|
||||||
|
icon: "info",
|
||||||
|
onSelect() {},
|
||||||
|
}],
|
||||||
|
folderActions: [{
|
||||||
|
label: "Export folder",
|
||||||
|
icon: "copy",
|
||||||
|
onSelect() {},
|
||||||
|
}],
|
||||||
|
async init() {},
|
||||||
|
};
|
||||||
|
"##,
|
||||||
|
)
|
||||||
|
.expect("write build/index.js");
|
||||||
|
|
||||||
|
generate_plugin_metadata(root).expect("generate metadata");
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(root.join("build/metadata.json")).expect("read metadata");
|
||||||
|
let metadata: Value = serde_json::from_str(&contents).expect("metadata json");
|
||||||
|
let api_types = metadata["apiTypes"].as_array().expect("apiTypes array");
|
||||||
|
|
||||||
|
for expected in [
|
||||||
|
"folderActions",
|
||||||
|
"templateFunctions",
|
||||||
|
"themes",
|
||||||
|
"workspaceActions",
|
||||||
|
"lifecycle",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
api_types.iter().any(|value| value.as_str() == Some(expected)),
|
||||||
|
"missing api type {expected}: {api_types:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(metadata["apis"]["themes"]["items"][0]["id"], "midnight");
|
||||||
|
assert_eq!(metadata["apis"]["workspaceActions"]["items"][0]["label"], "Sync workspace");
|
||||||
|
assert_eq!(metadata["apis"]["lifecycle"]["items"][0]["name"], "init");
|
||||||
|
assert!(metadata["apis"]["templateFunctions"]["items"][0]["onRender"].is_null());
|
||||||
|
assert!(
|
||||||
|
metadata["apis"]["templateFunctions"]["items"][0]["args"][0]["dynamic"].is_null()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,29 @@ async fn main() {
|
|||||||
|
|
||||||
let exit_code = match command {
|
let exit_code = match command {
|
||||||
Commands::Auth(args) => commands::auth::run(args).await,
|
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 {
|
Commands::Plugin(args) => match args.command {
|
||||||
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
|
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
|
||||||
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
|
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
|
||||||
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
|
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
|
||||||
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
|
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
|
||||||
|
PluginCommands::Metadata(args) => commands::plugin::run_metadata(args).await,
|
||||||
PluginCommands::Install(install_args) => {
|
PluginCommands::Install(install_args) => {
|
||||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||||
context.init_plugins(CliExecutionContext::default()).await;
|
context.init_plugins(CliExecutionContext::default()).await;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -38,6 +38,9 @@ pub enum Error {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ApiError(#[from] yaak_api::Error),
|
ApiError(#[from] yaak_api::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
YaakError(#[from] yaak::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ClipboardError(#[from] tauri_plugin_clipboard_manager::Error),
|
ClipboardError(#[from] tauri_plugin_clipboard_manager::Error),
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
use crate::PluginContextExt;
|
use crate::PluginContextExt;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
use log::info;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
|
use yaak::import::{self, ImportDataParams};
|
||||||
use yaak_core::WorkspaceContext;
|
use yaak_core::WorkspaceContext;
|
||||||
use yaak_models::models::{
|
use yaak_models::util::BatchUpsertResult;
|
||||||
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
|
||||||
};
|
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||||
|
|
||||||
@@ -19,113 +15,24 @@ pub(crate) async fn import_data<R: Runtime>(
|
|||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<BatchUpsertResult> {
|
) -> Result<BatchUpsertResult> {
|
||||||
let plugin_manager = window.state::<PluginManager>();
|
let plugin_manager = window.state::<PluginManager>();
|
||||||
|
let query_manager = window.db_manager();
|
||||||
let file = read_import_file(file_path)?;
|
let file = read_import_file(file_path)?;
|
||||||
let file_contents = file.as_str();
|
let plugin_context = window.plugin_context();
|
||||||
let import_result = plugin_manager.import_data(&window.plugin_context(), file_contents).await?;
|
let workspace_context = WorkspaceContext {
|
||||||
|
|
||||||
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
|
|
||||||
|
|
||||||
// Create WorkspaceContext from window
|
|
||||||
let ctx = WorkspaceContext {
|
|
||||||
workspace_id: window.workspace_id(),
|
workspace_id: window.workspace_id(),
|
||||||
environment_id: window.environment_id(),
|
environment_id: window.environment_id(),
|
||||||
cookie_jar_id: window.cookie_jar_id(),
|
cookie_jar_id: window.cookie_jar_id(),
|
||||||
request_id: None,
|
request_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let resources = import_result.resources;
|
Ok(import::import_data(ImportDataParams {
|
||||||
|
query_manager: &query_manager,
|
||||||
let workspaces: Vec<Workspace> = resources
|
plugin_manager: &plugin_manager,
|
||||||
.workspaces
|
plugin_context: &plugin_context,
|
||||||
.into_iter()
|
workspace_context,
|
||||||
.map(|mut v| {
|
contents: &file,
|
||||||
v.id = maybe_gen_id::<Workspace>(&ctx, v.id.as_str(), &mut id_map);
|
})
|
||||||
v
|
.await?)
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let environments: Vec<Environment> = resources
|
|
||||||
.environments
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut v| {
|
|
||||||
v.id = maybe_gen_id::<Environment>(&ctx, v.id.as_str(), &mut id_map);
|
|
||||||
v.workspace_id = maybe_gen_id::<Workspace>(&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::<Folder>(&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<Folder> = resources
|
|
||||||
.folders
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut v| {
|
|
||||||
v.id = maybe_gen_id::<Folder>(&ctx, v.id.as_str(), &mut id_map);
|
|
||||||
v.workspace_id = maybe_gen_id::<Workspace>(&ctx, v.workspace_id.as_str(), &mut id_map);
|
|
||||||
v.folder_id = maybe_gen_id_opt::<Folder>(&ctx, v.folder_id, &mut id_map);
|
|
||||||
v
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let http_requests: Vec<HttpRequest> = resources
|
|
||||||
.http_requests
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut v| {
|
|
||||||
v.id = maybe_gen_id::<HttpRequest>(&ctx, v.id.as_str(), &mut id_map);
|
|
||||||
v.workspace_id = maybe_gen_id::<Workspace>(&ctx, v.workspace_id.as_str(), &mut id_map);
|
|
||||||
v.folder_id = maybe_gen_id_opt::<Folder>(&ctx, v.folder_id, &mut id_map);
|
|
||||||
v
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let grpc_requests: Vec<GrpcRequest> = resources
|
|
||||||
.grpc_requests
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut v| {
|
|
||||||
v.id = maybe_gen_id::<GrpcRequest>(&ctx, v.id.as_str(), &mut id_map);
|
|
||||||
v.workspace_id = maybe_gen_id::<Workspace>(&ctx, v.workspace_id.as_str(), &mut id_map);
|
|
||||||
v.folder_id = maybe_gen_id_opt::<Folder>(&ctx, v.folder_id, &mut id_map);
|
|
||||||
v
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let websocket_requests: Vec<WebsocketRequest> = resources
|
|
||||||
.websocket_requests
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut v| {
|
|
||||||
v.id = maybe_gen_id::<WebsocketRequest>(&ctx, v.id.as_str(), &mut id_map);
|
|
||||||
v.workspace_id = maybe_gen_id::<Workspace>(&ctx, v.workspace_id.as_str(), &mut id_map);
|
|
||||||
v.folder_id = maybe_gen_id_opt::<Folder>(&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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_import_file(file_path: &str) -> Result<String> {
|
fn read_import_file(file_path: &str) -> Result<String> {
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ use error::Result as YaakResult;
|
|||||||
use eventsource_client::{EventParser, SSE};
|
use eventsource_client::{EventParser, SSE};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -31,6 +30,7 @@ use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
use yaak::export::{self, ExportDataParams};
|
||||||
use yaak_common::command::new_checked_command;
|
use yaak_common::command::new_checked_command;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
||||||
@@ -41,7 +41,7 @@ use yaak_models::models::{
|
|||||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Workspace,
|
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Workspace,
|
||||||
WorkspaceMeta,
|
WorkspaceMeta,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource};
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs,
|
CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs,
|
||||||
CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest,
|
CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest,
|
||||||
@@ -1384,24 +1384,14 @@ async fn cmd_export_data<R: Runtime>(
|
|||||||
workspace_ids: Vec<&str>,
|
workspace_ids: Vec<&str>,
|
||||||
include_private_environments: bool,
|
include_private_environments: bool,
|
||||||
) -> YaakResult<()> {
|
) -> YaakResult<()> {
|
||||||
let db = app_handle.db();
|
|
||||||
let version = app_handle.package_info().version.to_string();
|
let version = app_handle.package_info().version.to_string();
|
||||||
let export_data =
|
Ok(export::export_data(ExportDataParams {
|
||||||
get_workspace_export_resources(&db, &version, workspace_ids, include_private_environments)?;
|
query_manager: &app_handle.db_manager(),
|
||||||
let f = File::options()
|
yaak_version: &version,
|
||||||
.create(true)
|
export_path: Path::new(export_path),
|
||||||
.truncate(true)
|
workspace_ids,
|
||||||
.write(true)
|
include_private_environments,
|
||||||
.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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1425,11 +1415,10 @@ async fn cmd_send_http_request<R: Runtime>(
|
|||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
cookie_jar_id: Option<&str>,
|
||||||
// NOTE: We receive the entire request because to account for the race
|
request_id: String,
|
||||||
// condition where the user may have just edited a field before sending
|
|
||||||
// that has not yet been saved in the DB.
|
|
||||||
request: HttpRequest,
|
|
||||||
) -> YaakResult<HttpResponse> {
|
) -> YaakResult<HttpResponse> {
|
||||||
|
let request = app_handle.db().get_http_request(&request_id)?;
|
||||||
|
|
||||||
let blobs = app_handle.blob_manager();
|
let blobs = app_handle.blob_manager();
|
||||||
let response = app_handle.db().upsert_http_response(
|
let response = app_handle.db().upsert_http_response(
|
||||||
&HttpResponse {
|
&HttpResponse {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { newStoreData } from "./util";
|
|||||||
|
|
||||||
let _store: JotaiStore | null = null;
|
let _store: JotaiStore | null = null;
|
||||||
|
|
||||||
|
const pendingModelWrites = new Set<Promise<unknown>>();
|
||||||
|
|
||||||
export function initModelStore(store: JotaiStore) {
|
export function initModelStore(store: JotaiStore) {
|
||||||
_store = store;
|
_store = store;
|
||||||
|
|
||||||
@@ -42,6 +44,23 @@ function mustStore(): JotaiStore {
|
|||||||
return _store;
|
return _store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackModelWrite<T>(write: Promise<T>): Promise<T> {
|
||||||
|
const tracked = write.finally(() => {
|
||||||
|
pendingModelWrites.delete(tracked);
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingModelWrites.add(tracked);
|
||||||
|
return tracked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushAllModelWrites(): Promise<void> {
|
||||||
|
const results = await Promise.allSettled([...pendingModelWrites]);
|
||||||
|
const rejected = results.find((result) => result.status === "rejected");
|
||||||
|
if (rejected?.status === "rejected") {
|
||||||
|
throw rejected.reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _activeWorkspaceId: string | null = null;
|
let _activeWorkspaceId: string | null = null;
|
||||||
|
|
||||||
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
||||||
@@ -117,7 +136,7 @@ export async function patchModel<M extends AnyModel["model"], T extends ExtractM
|
|||||||
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
model: T,
|
model: T,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>("models_upsert", { model });
|
return trackModelWrite(invoke<string>("models_upsert", { model }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteModelById<
|
export async function deleteModelById<
|
||||||
@@ -134,7 +153,7 @@ export async function deleteModel<M extends AnyModel["model"], T extends Extract
|
|||||||
if (model == null) {
|
if (model == null) {
|
||||||
throw new Error("Failed to delete null model");
|
throw new Error("Failed to delete null model");
|
||||||
}
|
}
|
||||||
await invoke<string>("models_delete", { model });
|
await trackModelWrite(invoke<string>("models_delete", { model }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
export function duplicateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
@@ -174,19 +193,19 @@ export function duplicateModel<M extends AnyModel["model"], T extends ExtractMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return invoke<string>("models_duplicate", { model: { ...model, name } });
|
return trackModelWrite(invoke<string>("models_duplicate", { model: { ...model, name } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
||||||
patch: Partial<T> & Pick<T, "model">,
|
patch: Partial<T> & Pick<T, "model">,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>("models_upsert", { model: patch });
|
return trackModelWrite(invoke<string>("models_upsert", { model: patch }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
||||||
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
|
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>("models_upsert", { model: patch });
|
return trackModelWrite(invoke<string>("models_upsert", { model: patch }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceModelsInStore<
|
export function replaceModelsInStore<
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ serde_json = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync", "rt"] }
|
tokio = { workspace = true, features = ["sync", "rt"] }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
|
yaak-core = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
yaak-plugins = { workspace = true }
|
yaak-plugins = { workspace = true }
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ use thiserror::Error;
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Send(#[from] crate::send::SendHttpRequestError),
|
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<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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<BatchUpsertResult> {
|
||||||
|
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<BatchUpsertResult> {
|
||||||
|
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
|
||||||
|
let workspaces: Vec<Workspace> = resources
|
||||||
|
.workspaces
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<Workspace>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let environments: Vec<Environment> = resources
|
||||||
|
.environments
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<Environment>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v.workspace_id =
|
||||||
|
maybe_gen_id::<Workspace>(&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::<Folder>(&workspace_context, parent_id, &mut id_map));
|
||||||
|
}
|
||||||
|
("", _) => {
|
||||||
|
v.parent_model = "workspace".to_string();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
v.parent_id = None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let folders: Vec<Folder> = resources
|
||||||
|
.folders
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<Folder>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v.workspace_id =
|
||||||
|
maybe_gen_id::<Workspace>(&workspace_context, v.workspace_id.as_str(), &mut id_map);
|
||||||
|
v.folder_id = maybe_gen_id_opt::<Folder>(&workspace_context, v.folder_id, &mut id_map);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let http_requests: Vec<HttpRequest> = resources
|
||||||
|
.http_requests
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<HttpRequest>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v.workspace_id =
|
||||||
|
maybe_gen_id::<Workspace>(&workspace_context, v.workspace_id.as_str(), &mut id_map);
|
||||||
|
v.folder_id = maybe_gen_id_opt::<Folder>(&workspace_context, v.folder_id, &mut id_map);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let grpc_requests: Vec<GrpcRequest> = resources
|
||||||
|
.grpc_requests
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<GrpcRequest>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v.workspace_id =
|
||||||
|
maybe_gen_id::<Workspace>(&workspace_context, v.workspace_id.as_str(), &mut id_map);
|
||||||
|
v.folder_id = maybe_gen_id_opt::<Folder>(&workspace_context, v.folder_id, &mut id_map);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let websocket_requests: Vec<WebsocketRequest> = resources
|
||||||
|
.websocket_requests
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut v| {
|
||||||
|
v.id = maybe_gen_id::<WebsocketRequest>(&workspace_context, v.id.as_str(), &mut id_map);
|
||||||
|
v.workspace_id =
|
||||||
|
maybe_gen_id::<Workspace>(&workspace_context, v.workspace_id.as_str(), &mut id_map);
|
||||||
|
v.folder_id = maybe_gen_id_opt::<Folder>(&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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod export;
|
||||||
|
pub mod import;
|
||||||
pub mod plugin_events;
|
pub mod plugin_events;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod send;
|
pub mod send;
|
||||||
|
|||||||
Generated
+18
@@ -16970,6 +16970,7 @@
|
|||||||
"ws": "^8.20.1"
|
"ws": "^8.20.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.13",
|
||||||
"@types/ws": "^8.5.13"
|
"@types/ws": "^8.5.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -16991,6 +16992,23 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/plugin-runtime/node_modules/@types/node": {
|
||||||
|
"version": "24.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||||
|
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/plugin-runtime/node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"packages/tailwind-config": {
|
"packages/tailwind-config": {
|
||||||
"name": "@yaakapp-internal/tailwind-config",
|
"name": "@yaakapp-internal/tailwind-config",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
24.11.1
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"ws": "^8.20.1"
|
"ws": "^8.20.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.13",
|
||||||
"@types/ws": "^8.5.13"
|
"@types/ws": "^8.5.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import path from "node:path";
|
||||||
|
import type { PluginDefinition } from "@yaakapp/api";
|
||||||
|
|
||||||
|
type PluginFeatureKey = Exclude<
|
||||||
|
Extract<keyof PluginDefinition, string>,
|
||||||
|
"init" | "dispose"
|
||||||
|
>;
|
||||||
|
type PluginAPIKey = PluginFeatureKey | "lifecycle";
|
||||||
|
|
||||||
|
type MetadataDefinition = {
|
||||||
|
key: PluginFeatureKey;
|
||||||
|
label: string;
|
||||||
|
array: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MetadataItem =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| MetadataItem[]
|
||||||
|
| { [key: string]: MetadataItem };
|
||||||
|
|
||||||
|
type APITypeMetadata = {
|
||||||
|
label: string;
|
||||||
|
source: string;
|
||||||
|
count: number;
|
||||||
|
items: MetadataItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PluginMetadata = {
|
||||||
|
schemaVersion: 1;
|
||||||
|
apiTypes: PluginAPIKey[];
|
||||||
|
apis: Partial<Record<PluginAPIKey, APITypeMetadata>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions: MetadataDefinition[] = [
|
||||||
|
{
|
||||||
|
key: "authentication",
|
||||||
|
label: "Authentication",
|
||||||
|
array: false,
|
||||||
|
},
|
||||||
|
{ key: "filter", label: "Filter", array: false },
|
||||||
|
{
|
||||||
|
key: "folderActions",
|
||||||
|
label: "Folder Action",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "grpcRequestActions",
|
||||||
|
label: "gRPC Request Action",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "httpRequestActions",
|
||||||
|
label: "HTTP Request Action",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
{ key: "importer", label: "Importer", array: false },
|
||||||
|
{
|
||||||
|
key: "templateFunctions",
|
||||||
|
label: "Template Tag",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
{ key: "themes", label: "Theme", array: true },
|
||||||
|
{
|
||||||
|
key: "websocketRequestActions",
|
||||||
|
label: "WebSocket Request Action",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "workspaceActions",
|
||||||
|
label: "Workspace Action",
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function generatePluginMetadata(
|
||||||
|
plugin: PluginDefinition,
|
||||||
|
): PluginMetadata {
|
||||||
|
const metadata: PluginMetadata = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
apiTypes: [],
|
||||||
|
apis: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const definition of definitions) {
|
||||||
|
const value = plugin[definition.key];
|
||||||
|
const items = definition.array ? value : value ? [value] : [];
|
||||||
|
|
||||||
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.apiTypes.push(definition.key);
|
||||||
|
metadata.apis[definition.key] = {
|
||||||
|
label: definition.label,
|
||||||
|
source: definition.key,
|
||||||
|
count: items.length,
|
||||||
|
items: sanitize(items) as MetadataItem[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lifecycleHooks = ["init", "dispose"].filter(
|
||||||
|
(key) =>
|
||||||
|
typeof plugin[key as keyof Pick<PluginDefinition, "init" | "dispose">] ===
|
||||||
|
"function",
|
||||||
|
);
|
||||||
|
if (lifecycleHooks.length > 0) {
|
||||||
|
metadata.apiTypes.push("lifecycle");
|
||||||
|
metadata.apis.lifecycle = {
|
||||||
|
label: "Lifecycle Hook",
|
||||||
|
source: lifecycleHooks.join(","),
|
||||||
|
count: lifecycleHooks.length,
|
||||||
|
items: lifecycleHooks.map((name) => ({ name })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPath = process.argv[1];
|
||||||
|
const outputPath = process.argv[2];
|
||||||
|
|
||||||
|
if (!entryPath) {
|
||||||
|
throw new Error("Missing plugin entrypoint path");
|
||||||
|
}
|
||||||
|
if (!outputPath) {
|
||||||
|
throw new Error("Missing plugin metadata output path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const require = createRequire(path.join(process.cwd(), "plugin-metadata.js"));
|
||||||
|
const moduleExports = require(path.resolve(entryPath)) as PluginDefinition & {
|
||||||
|
plugin?: PluginDefinition;
|
||||||
|
default?: PluginDefinition;
|
||||||
|
};
|
||||||
|
const plugin = moduleExports.plugin ?? moduleExports.default ?? moduleExports;
|
||||||
|
|
||||||
|
if (!plugin || typeof plugin !== "object") {
|
||||||
|
throw new Error("Plugin entrypoint must export a plugin object");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = generatePluginMetadata(plugin);
|
||||||
|
fs.writeFileSync(outputPath, `${JSON.stringify(metadata, null, 2)}\n`);
|
||||||
|
|
||||||
|
function sanitize(
|
||||||
|
value: unknown,
|
||||||
|
seen = new WeakSet<object>(),
|
||||||
|
): MetadataItem | undefined {
|
||||||
|
if (value === null) return null;
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case "boolean":
|
||||||
|
case "number":
|
||||||
|
case "string":
|
||||||
|
return value;
|
||||||
|
case "bigint":
|
||||||
|
return value.toString();
|
||||||
|
case "function":
|
||||||
|
case "symbol":
|
||||||
|
case "undefined":
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectValue = value as object;
|
||||||
|
if (seen.has(objectValue)) {
|
||||||
|
return "[Circular]";
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(objectValue);
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const output = value.map((item) => sanitize(item, seen) ?? null);
|
||||||
|
seen.delete(objectValue);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: Record<string, MetadataItem> = {};
|
||||||
|
for (const [key, item] of Object.entries(objectValue)) {
|
||||||
|
const sanitized = sanitize(item, seen);
|
||||||
|
if (sanitized !== undefined) {
|
||||||
|
output[key] = sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.delete(objectValue);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@@ -364,6 +364,8 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
ref={handleEditFocus}
|
ref={handleEditFocus}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
className="bg-transparent outline-none w-full cursor-text"
|
className="bg-transparent outline-none w-full cursor-text"
|
||||||
onBlur={handleEditBlur}
|
onBlur={handleEditBlur}
|
||||||
onKeyDown={handleEditKeyDown}
|
onKeyDown={handleEditKeyDown}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const Downloader = require("nodejs-file-downloader");
|
|||||||
const { rmSync, cpSync, mkdirSync, existsSync } = require("node:fs");
|
const { rmSync, cpSync, mkdirSync, existsSync } = require("node:fs");
|
||||||
const { execSync } = require("node:child_process");
|
const { execSync } = require("node:child_process");
|
||||||
|
|
||||||
const NODE_VERSION = "v24.11.1";
|
const nodeVersionFile = path.join(__dirname, "..", "packages", "plugin-runtime", ".node-version");
|
||||||
|
const NODE_VERSION = `v${fs.readFileSync(nodeVersionFile, "utf8").trim().replace(/^v/, "")}`;
|
||||||
|
|
||||||
// `${process.platform}_${process.arch}`
|
// `${process.platform}_${process.arch}`
|
||||||
const MAC_ARM = "darwin_arm64";
|
const MAC_ARM = "darwin_arm64";
|
||||||
|
|||||||
Reference in New Issue
Block a user