mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 15:47:47 +01:00
Compare commits
82 Commits
v2025.2.0-
...
v2025.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72dd768f55 | ||
|
|
862d85e48d | ||
|
|
a6d03cbeeb | ||
|
|
7d1ca1c232 | ||
|
|
261911b57e | ||
|
|
245054cd7d | ||
|
|
101582e540 | ||
|
|
0a932798a0 | ||
|
|
4609c95ad5 | ||
|
|
9d54e40aa8 | ||
|
|
9ec9222216 | ||
|
|
4d1dda0786 | ||
|
|
31605881ac | ||
|
|
4cd2e9cd31 | ||
|
|
13d959799a | ||
|
|
a6b18c23e1 | ||
|
|
041298b3f8 | ||
|
|
b400940f0e | ||
|
|
2e144f064d | ||
|
|
d8b1cadae6 | ||
|
|
c2f9760d08 | ||
|
|
a4c600cb48 | ||
|
|
bc3a5e3e58 | ||
|
|
4c3a02ac53 | ||
|
|
1974d61aa4 | ||
|
|
0bcb092854 | ||
|
|
409620f533 | ||
|
|
3e9037f70a | ||
|
|
be82b67ed3 | ||
|
|
432b366105 | ||
|
|
42e70b941d | ||
|
|
3808215210 | ||
|
|
763a60982a | ||
|
|
a05679fd93 | ||
|
|
73c366dc27 | ||
|
|
0be7d0283b | ||
|
|
749df338c5 | ||
|
|
3184c1b79e | ||
|
|
b52bf7cd56 | ||
|
|
d962d7f94b | ||
|
|
21e2a67c1e | ||
|
|
c188435524 | ||
|
|
8a7a7ba49d | ||
|
|
cbc40230bb | ||
|
|
bc4c3178c9 | ||
|
|
121fe5b3ea | ||
|
|
861609ddc0 | ||
|
|
e5070513ac | ||
|
|
f5c3798df9 | ||
|
|
469d12fede | ||
|
|
417a02744b | ||
|
|
81e78ef24c | ||
|
|
dad9cebb9e | ||
|
|
b3ede3d6d6 | ||
|
|
035fe54df0 | ||
|
|
5f8d99ba64 | ||
|
|
84b8d130dc | ||
|
|
94d4227bc1 | ||
|
|
77cdea2f9f | ||
|
|
8b1ca4cb47 | ||
|
|
d3b8a42180 | ||
|
|
95f39c514a | ||
|
|
7cba082eb0 | ||
|
|
3b9b320be2 | ||
|
|
18664975a9 | ||
|
|
bb014b7c43 | ||
|
|
9fa0650647 | ||
|
|
b8c42677ca | ||
|
|
2eb3c2241c | ||
|
|
8fb7bbfe2e | ||
|
|
52eba74151 | ||
|
|
e651760713 | ||
|
|
82451a26f6 | ||
|
|
cc15f60fb6 | ||
|
|
2f8b2a81c7 | ||
|
|
6d4fdc91fe | ||
|
|
faca29c789 | ||
|
|
1ab937aae4 | ||
|
|
45fcea1954 | ||
|
|
73554078d1 | ||
|
|
a42a88de7b | ||
|
|
14a6079176 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: cargo install --force trusted-signing-cli
|
||||
run: cargo install --force trusted-signing-cli --version 0.5.0
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: |
|
||||
|
||||
25
README.md
25
README.md
@@ -3,7 +3,12 @@
|
||||
Yaak is a desktop API client for interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC
|
||||
APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
|
||||

|
||||

|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak is open source, but only accepting contributions for bug fixes. To get started,
|
||||
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
|
||||
|
||||
## Feature Overview
|
||||
|
||||
@@ -14,6 +19,7 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
|
||||
- 📂 Organize requests into workspaces and nested folders.<br/>
|
||||
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
|
||||
- 🛡️ Secure arbitrary text values with end-to-end encryption<br/>
|
||||
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
|
||||
- 🎨 Choose from many of the included themes, or make your own.<br/>
|
||||
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
|
||||
@@ -21,17 +27,8 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
|
||||
- 🛜 Configure a proxy to access firewall-blocked APIs
|
||||
|
||||
## Feedback and Bug Reports
|
||||
## Useful Resources
|
||||
|
||||
All feedback, bug reports, questions, and feature requests should be reported to
|
||||
[feedback.yaak.app](https://feedback.yaak.app).
|
||||
|
||||
## Community Projects
|
||||
|
||||
- [`yaak2postman`](https://github.com/BiteCraft/yaak2postman) CLI for converting Yaak data
|
||||
exports to Postman-compatible collections
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak is open source, but only accepting contributions for bug fixes. To get started,
|
||||
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
|
||||
- [Feedback and Bug Reports](https://feedback.yaak.app)
|
||||
- [Documentation](https://feedback.yaak.app/help)
|
||||
- [Yaak vs Postman](https://yaak.app/blog/postman-alternative)
|
||||
|
||||
2603
package-lock.json
generated
2603
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -104,7 +104,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputCheckbox = {
|
||||
/**
|
||||
@@ -131,7 +135,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputEditor = {
|
||||
/**
|
||||
@@ -170,7 +178,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputFile = {
|
||||
/**
|
||||
@@ -205,7 +217,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
@@ -232,7 +248,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||
|
||||
@@ -265,7 +285,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputSelectOption = { label: string, value: string, };
|
||||
|
||||
@@ -306,10 +330,18 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
|
||||
|
||||
export type GetCookieValueRequest = { name: string, };
|
||||
|
||||
export type GetCookieValueResponse = { value: string | null, };
|
||||
|
||||
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
|
||||
|
||||
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
|
||||
@@ -346,10 +378,14 @@ export type ImportResponse = { resources: ImportResources, };
|
||||
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
export type ListCookieNamesRequest = {};
|
||||
|
||||
export type ListCookieNamesResponse = { names: Array<string>, };
|
||||
|
||||
export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* Label for the window. If not provided, a random one will be generated.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -3,3 +3,7 @@ export type * from './themes';
|
||||
|
||||
export * from './bindings/gen_models';
|
||||
export * from './bindings/gen_events';
|
||||
|
||||
// Some extras for utility
|
||||
|
||||
export type { PartialImportResources } from './plugins/ImporterPlugin';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type {
|
||||
FindHttpResponsesRequest,
|
||||
FindHttpResponsesResponse,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdRequest,
|
||||
GetHttpRequestByIdResponse,
|
||||
ListCookieNamesResponse,
|
||||
OpenWindowRequest,
|
||||
PromptTextRequest,
|
||||
PromptTextResponse,
|
||||
@@ -34,10 +37,14 @@ export interface Context {
|
||||
openUrl(
|
||||
args: OpenWindowRequest & {
|
||||
onNavigate?: (args: { url: string }) => void;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
},
|
||||
): Promise<{ close: () => void }>;
|
||||
};
|
||||
cookies: {
|
||||
listNames(): Promise<ListCookieNamesResponse['names']>;
|
||||
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
|
||||
};
|
||||
httpRequest: {
|
||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Context } from './Context';
|
||||
|
||||
export type FilterPluginResponse = { filtered: string };
|
||||
type FilterPluginResponse = { filtered: string };
|
||||
|
||||
export type FilterPlugin = {
|
||||
name: string;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../bindings/gen_models';
|
||||
import type { AtLeast } from '../helpers';
|
||||
import { ImportResources } from '../bindings/gen_events';
|
||||
import { AtLeast } from '../helpers';
|
||||
import type { Context } from './Context';
|
||||
|
||||
type ImportPluginResponse = null | {
|
||||
resources: {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
};
|
||||
type RootFields = 'name' | 'id' | 'model';
|
||||
type CommonFields = RootFields | 'workspaceId';
|
||||
|
||||
export type PartialImportResources = {
|
||||
workspaces: Array<AtLeast<ImportResources['workspaces'][0], RootFields>>;
|
||||
environments: Array<AtLeast<ImportResources['environments'][0], CommonFields>>;
|
||||
folders: Array<AtLeast<ImportResources['folders'][0], CommonFields>>;
|
||||
httpRequests: Array<AtLeast<ImportResources['httpRequests'][0], CommonFields>>;
|
||||
grpcRequests: Array<AtLeast<ImportResources['grpcRequests'][0], CommonFields>>;
|
||||
websocketRequests: Array<AtLeast<ImportResources['websocketRequests'][0], CommonFields>>;
|
||||
};
|
||||
|
||||
export type ImportPluginResponse = null | {
|
||||
resources: PartialImportResources;
|
||||
};
|
||||
|
||||
export type ImporterPlugin = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||
import type {
|
||||
import {
|
||||
BootRequest,
|
||||
Context,
|
||||
DeleteKeyValueResponse,
|
||||
FindHttpResponsesResponse,
|
||||
FormInput,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdResponse,
|
||||
GetKeyValueResponse,
|
||||
HttpAuthenticationAction,
|
||||
@@ -12,19 +12,20 @@ import type {
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
ListCookieNamesResponse,
|
||||
PluginWindowContext,
|
||||
PromptTextResponse,
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
TemplateFunctionArg,
|
||||
TemplateRenderResponse,
|
||||
} from '@yaakapp/api';
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import console from 'node:console';
|
||||
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
// import util from 'node:util';
|
||||
import { EventChannel } from './EventChannel';
|
||||
// import { interceptStdout } from './interceptStdout';
|
||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||
|
||||
export interface PluginWorkerData {
|
||||
@@ -495,6 +496,27 @@ export class PluginInstance {
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
cookies: {
|
||||
getValue: async (args: GetCookieValueRequest) => {
|
||||
const payload = {
|
||||
type: 'get_cookie_value_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return value;
|
||||
},
|
||||
listNames: async () => {
|
||||
const payload = { type: 'list_cookie_names_request' } as const;
|
||||
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return names;
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
/**
|
||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||
|
||||
@@ -9,12 +9,14 @@ const NODE_VERSION = 'v22.9.0';
|
||||
// `${process.platform}_${process.arch}`
|
||||
const MAC_ARM = 'darwin_arm64';
|
||||
const MAC_X64 = 'darwin_x64';
|
||||
const LNX_ARM = 'linux_arm64';
|
||||
const LNX_X64 = 'linux_x64';
|
||||
const WIN_X64 = 'win32_x64';
|
||||
|
||||
const URL_MAP = {
|
||||
[MAC_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||||
[MAC_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-x64.tar.gz`,
|
||||
[LNX_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz`,
|
||||
[LNX_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz`,
|
||||
[WIN_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-win-x64.zip`,
|
||||
};
|
||||
@@ -22,15 +24,17 @@ const URL_MAP = {
|
||||
const SRC_BIN_MAP = {
|
||||
[MAC_ARM]: `node-${NODE_VERSION}-darwin-arm64/bin/node`,
|
||||
[MAC_X64]: `node-${NODE_VERSION}-darwin-x64/bin/node`,
|
||||
[LNX_ARM]: `node-${NODE_VERSION}-linux-arm64/bin/node`,
|
||||
[LNX_X64]: `node-${NODE_VERSION}-linux-x64/bin/node`,
|
||||
[WIN_X64]: `node-${NODE_VERSION}-win-x64/node.exe`,
|
||||
};
|
||||
|
||||
const DST_BIN_MAP = {
|
||||
darwin_arm64: 'yaaknode-aarch64-apple-darwin',
|
||||
darwin_x64: 'yaaknode-x86_64-apple-darwin',
|
||||
linux_x64: 'yaaknode-x86_64-unknown-linux-gnu',
|
||||
win32_x64: 'yaaknode-x86_64-pc-windows-msvc.exe',
|
||||
[MAC_ARM]: 'yaaknode-aarch64-apple-darwin',
|
||||
[MAC_X64]: 'yaaknode-x86_64-apple-darwin',
|
||||
[LNX_ARM]: 'yaaknode-aarch64-unknown-linux-gnu',
|
||||
[LNX_X64]: 'yaaknode-x86_64-unknown-linux-gnu',
|
||||
[WIN_X64]: 'yaaknode-x86_64-pc-windows-msvc.exe',
|
||||
};
|
||||
|
||||
const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`;
|
||||
|
||||
@@ -9,12 +9,14 @@ const VERSION = '28.3';
|
||||
// `${process.platform}_${process.arch}`
|
||||
const MAC_ARM = 'darwin_arm64';
|
||||
const MAC_X64 = 'darwin_x64';
|
||||
const LNX_ARM = 'linux_arm64';
|
||||
const LNX_X64 = 'linux_x64';
|
||||
const WIN_X64 = 'win32_x64';
|
||||
|
||||
const URL_MAP = {
|
||||
[MAC_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-aarch_64.zip`,
|
||||
[MAC_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-x86_64.zip`,
|
||||
[LNX_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-aarch_64.zip`,
|
||||
[LNX_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-x86_64.zip`,
|
||||
[WIN_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-win64.zip`,
|
||||
};
|
||||
@@ -22,6 +24,7 @@ const URL_MAP = {
|
||||
const SRC_BIN_MAP = {
|
||||
[MAC_ARM]: 'bin/protoc',
|
||||
[MAC_X64]: 'bin/protoc',
|
||||
[LNX_ARM]: 'bin/protoc',
|
||||
[LNX_X64]: 'bin/protoc',
|
||||
[WIN_X64]: 'bin/protoc.exe',
|
||||
};
|
||||
@@ -29,6 +32,7 @@ const SRC_BIN_MAP = {
|
||||
const DST_BIN_MAP = {
|
||||
[MAC_ARM]: 'yaakprotoc-aarch64-apple-darwin',
|
||||
[MAC_X64]: 'yaakprotoc-x86_64-apple-darwin',
|
||||
[LNX_ARM]: 'yaakprotoc-aarch64-unknown-linux-gnu',
|
||||
[LNX_X64]: 'yaakprotoc-x86_64-unknown-linux-gnu',
|
||||
[WIN_X64]: 'yaakprotoc-x86_64-pc-windows-msvc.exe',
|
||||
};
|
||||
|
||||
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
@@ -1258,7 +1258,6 @@ dependencies = [
|
||||
"hkdf",
|
||||
"num",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"rand 0.8.5",
|
||||
"sha2",
|
||||
]
|
||||
@@ -2875,7 +2874,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -8043,6 +8041,7 @@ name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cookie",
|
||||
"encoding_rs",
|
||||
"eventsource-client",
|
||||
"http",
|
||||
|
||||
@@ -40,6 +40,7 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
encoding_rs = "0.8.35"
|
||||
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
|
||||
http = { version = "1.2.0", default-features = false }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:app:allow-identifier",
|
||||
"core:event:allow-emit",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE settings
|
||||
ADD COLUMN hide_window_controls BOOLEAN DEFAULT FALSE NOT NULL;
|
||||
245
src-tauri/migrations/20250424152740_remove-fks.sql
Normal file
245
src-tauri/migrations/20250424152740_remove-fks.sql
Normal file
@@ -0,0 +1,245 @@
|
||||
-- NOTE: SQLite does not support dropping foreign keys, so we need to create new
|
||||
-- tables and copy data instead. To prevent cascade deletes from wrecking stuff,
|
||||
-- we start with the leaf tables and finish with the parent tables (eg. folder).
|
||||
|
||||
----------------------------
|
||||
-- Remove http request FK --
|
||||
----------------------------
|
||||
|
||||
CREATE TABLE http_requests_dg_tmp
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'http_request' NOT NULL,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
headers TEXT NOT NULL,
|
||||
body_type TEXT,
|
||||
sort_priority REAL DEFAULT 0 NOT NULL,
|
||||
authentication TEXT DEFAULT '{}' NOT NULL,
|
||||
authentication_type TEXT,
|
||||
folder_id TEXT,
|
||||
body TEXT DEFAULT '{}' NOT NULL,
|
||||
url_parameters TEXT DEFAULT '[]' NOT NULL,
|
||||
description TEXT DEFAULT '' NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO http_requests_dg_tmp(id, model, workspace_id, created_at, updated_at, deleted_at, name, url, method,
|
||||
headers, body_type, sort_priority, authentication, authentication_type, folder_id,
|
||||
body, url_parameters, description)
|
||||
SELECT id,
|
||||
model,
|
||||
workspace_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
name,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body_type,
|
||||
sort_priority,
|
||||
authentication,
|
||||
authentication_type,
|
||||
folder_id,
|
||||
body,
|
||||
url_parameters,
|
||||
description
|
||||
FROM http_requests;
|
||||
|
||||
DROP TABLE http_requests;
|
||||
|
||||
ALTER TABLE http_requests_dg_tmp
|
||||
RENAME TO http_requests;
|
||||
|
||||
----------------------------
|
||||
-- Remove grpc request FK --
|
||||
----------------------------
|
||||
|
||||
CREATE TABLE grpc_requests_dg_tmp
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'grpc_request' NOT NULL,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
folder_id TEXT,
|
||||
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
sort_priority REAL NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
service TEXT,
|
||||
method TEXT,
|
||||
message TEXT NOT NULL,
|
||||
authentication TEXT DEFAULT '{}' NOT NULL,
|
||||
authentication_type TEXT,
|
||||
metadata TEXT DEFAULT '[]' NOT NULL,
|
||||
description TEXT DEFAULT '' NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO grpc_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, url,
|
||||
service, method, message, authentication, authentication_type, metadata, description)
|
||||
SELECT id,
|
||||
model,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
sort_priority,
|
||||
url,
|
||||
service,
|
||||
method,
|
||||
message,
|
||||
authentication,
|
||||
authentication_type,
|
||||
metadata,
|
||||
description
|
||||
FROM grpc_requests;
|
||||
|
||||
DROP TABLE grpc_requests;
|
||||
|
||||
ALTER TABLE grpc_requests_dg_tmp
|
||||
RENAME TO grpc_requests;
|
||||
|
||||
---------------------------------
|
||||
-- Remove websocket request FK --
|
||||
---------------------------------
|
||||
|
||||
CREATE TABLE websocket_requests_dg_tmp
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'websocket_request' NOT NULL,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
folder_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
authentication TEXT DEFAULT '{}' NOT NULL,
|
||||
authentication_type TEXT,
|
||||
description TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
headers TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
sort_priority REAL NOT NULL,
|
||||
url_parameters TEXT DEFAULT '[]' NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO websocket_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, deleted_at,
|
||||
authentication, authentication_type, description, name, url, headers, message,
|
||||
sort_priority, url_parameters)
|
||||
SELECT id,
|
||||
model,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
authentication,
|
||||
authentication_type,
|
||||
description,
|
||||
name,
|
||||
url,
|
||||
headers,
|
||||
message,
|
||||
sort_priority,
|
||||
url_parameters
|
||||
FROM websocket_requests;
|
||||
|
||||
DROP TABLE websocket_requests;
|
||||
|
||||
ALTER TABLE websocket_requests_dg_tmp
|
||||
RENAME TO websocket_requests;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
---------------------------
|
||||
-- Remove environment FK --
|
||||
---------------------------
|
||||
|
||||
CREATE TABLE environments_dg_tmp
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
variables DEFAULT '[]' NOT NULL,
|
||||
model TEXT DEFAULT 'environment',
|
||||
environment_id TEXT
|
||||
);
|
||||
|
||||
INSERT INTO environments_dg_tmp(id, created_at, updated_at, deleted_at, workspace_id, name, variables, model,
|
||||
environment_id)
|
||||
SELECT id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
workspace_id,
|
||||
name,
|
||||
variables,
|
||||
model,
|
||||
environment_id
|
||||
FROM environments;
|
||||
|
||||
DROP TABLE environments;
|
||||
|
||||
ALTER TABLE environments_dg_tmp
|
||||
RENAME TO environments;
|
||||
|
||||
----------------------
|
||||
-- Remove folder FK --
|
||||
----------------------
|
||||
|
||||
CREATE TABLE folders_dg_tmp
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'folder' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
folder_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
sort_priority REAL DEFAULT 0 NOT NULL,
|
||||
description TEXT DEFAULT '' NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO folders_dg_tmp(id, model, created_at, updated_at, deleted_at, workspace_id, folder_id, name, sort_priority,
|
||||
description)
|
||||
SELECT id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
name,
|
||||
sort_priority,
|
||||
description
|
||||
FROM folders;
|
||||
|
||||
DROP TABLE folders;
|
||||
|
||||
ALTER TABLE folders_dg_tmp
|
||||
RENAME TO folders;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- There used to be sync code that skipped over environments because we didn't
|
||||
-- want to sync potentially insecure data. With encryption, it is now possible
|
||||
-- to sync environments securely. However, there were already sync states in the
|
||||
-- DB that marked environments as "Synced". Running the sync code on these envs
|
||||
-- would mark them as deleted by FS (exist in SyncState but not on FS).
|
||||
--
|
||||
-- To undo this mess, we have this migration to delete all environment-related
|
||||
-- sync states so we can sync from a clean slate.
|
||||
DELETE
|
||||
FROM sync_states
|
||||
WHERE model_id LIKE 'ev_%';
|
||||
20
src-tauri/migrations/20250508161145_public-environments.sql
Normal file
20
src-tauri/migrations/20250508161145_public-environments.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Add a public column to represent whether an environment can be shared or exported
|
||||
ALTER TABLE environments
|
||||
ADD COLUMN public BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- Add a base column to represent whether an environment is a base or sub environment. We used to
|
||||
-- do this with environment_id, but we need a more flexible solution now that envs can be optionally
|
||||
-- synced. E.g., it's now possible to only import a sub environment from a different client without
|
||||
-- its base environment "parent."
|
||||
ALTER TABLE environments
|
||||
ADD COLUMN base BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- SQLite doesn't support dynamic default values, so we update `base` based on the value of
|
||||
-- environment_id.
|
||||
UPDATE environments
|
||||
SET base = TRUE
|
||||
WHERE environment_id IS NULL;
|
||||
|
||||
-- Finally, we drop the old `environment_id` column that will no longer be used
|
||||
ALTER TABLE environments
|
||||
DROP COLUMN environment_id;
|
||||
15
src-tauri/migrations/20250516182745_default-attrs.sql
Normal file
15
src-tauri/migrations/20250516182745_default-attrs.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Auth
|
||||
ALTER TABLE workspaces
|
||||
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
|
||||
ALTER TABLE folders
|
||||
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
|
||||
ALTER TABLE workspaces
|
||||
ADD COLUMN authentication_type TEXT;
|
||||
ALTER TABLE folders
|
||||
ADD COLUMN authentication_type TEXT;
|
||||
|
||||
-- Headers
|
||||
ALTER TABLE workspaces
|
||||
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';
|
||||
ALTER TABLE folders
|
||||
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::error::Result;
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||
use yaak_models::models::GrpcRequest;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use KeyAndValueRef::{Ascii, Binary};
|
||||
|
||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||
|
||||
pub fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
|
||||
pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
|
||||
let mut entries = BTreeMap::new();
|
||||
for r in metadata.iter() {
|
||||
match r {
|
||||
@@ -14,3 +19,74 @@ pub fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_grpc_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &GrpcRequest,
|
||||
) -> Result<GrpcRequest> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication) =
|
||||
window.db().resolve_auth_for_grpc_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let metadata = window.db().resolve_metadata_for_grpc_request(request)?;
|
||||
new_request.metadata = metadata;
|
||||
|
||||
Ok(new_request)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_metadata<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &GrpcRequest,
|
||||
) -> Result<BTreeMap<String, String>> {
|
||||
let plugin_manager = window.state::<PluginManager>();
|
||||
let mut metadata = BTreeMap::new();
|
||||
|
||||
// Add the rest of metadata
|
||||
for h in request.metadata.clone() {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !h.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata.insert(h.name, h.value);
|
||||
}
|
||||
|
||||
match request.authentication_type.clone() {
|
||||
None => {
|
||||
// No authentication found. Not even inherited
|
||||
}
|
||||
Some(authentication_type) if authentication_type == "none" => {
|
||||
// Explicitly no authentication
|
||||
}
|
||||
Some(authentication_type) => {
|
||||
let auth = request.authentication.clone();
|
||||
let plugin_req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request.id.clone())),
|
||||
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
||||
method: "POST".to_string(),
|
||||
url: request.url.clone(),
|
||||
headers: metadata
|
||||
.iter()
|
||||
.map(|(name, value)| HttpHeader {
|
||||
name: name.to_string(),
|
||||
value: value.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let plugin_result = plugin_manager
|
||||
.call_http_authentication(&window, &authentication_type, plugin_req)
|
||||
.await?;
|
||||
for header in plugin_result.set_headers {
|
||||
metadata.insert(header.name, header.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ use crate::error::Result;
|
||||
use crate::render::render_http_request;
|
||||
use crate::response_err;
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use log::{debug, error, warn};
|
||||
use mime_guess::Mime;
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::{multipart, Proxy, Url};
|
||||
use reqwest::{Method, Response};
|
||||
use rustls::crypto::ring;
|
||||
use reqwest::{Proxy, Url, multipart};
|
||||
use rustls::ClientConfig;
|
||||
use rustls::crypto::ring;
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -20,10 +20,10 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use tokio::fs;
|
||||
use tokio::fs::{create_dir_all, File};
|
||||
use tokio::fs::{File, create_dir_all};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::watch::Receiver;
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
use tokio::sync::{Mutex, oneshot};
|
||||
use yaak_models::models::{
|
||||
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
|
||||
HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||
@@ -65,14 +65,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
);
|
||||
let update_source = UpdateSource::from_window(window);
|
||||
|
||||
let request = match render_http_request(
|
||||
&unrendered_request,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
&cb,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let resolved_request = match resolve_http_request(window, unrendered_request) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
@@ -80,11 +73,26 @@ pub async fn send_http_request<R: Runtime>(
|
||||
&*response.lock().await,
|
||||
e.to_string(),
|
||||
&update_source,
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut url_string = request.url;
|
||||
let request =
|
||||
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&app_handle,
|
||||
&*response.lock().await,
|
||||
e.to_string(),
|
||||
&update_source,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut url_string = request.url.clone();
|
||||
|
||||
url_string = ensure_proto(&url_string);
|
||||
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
|
||||
@@ -123,7 +131,12 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
match settings.proxy {
|
||||
Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(),
|
||||
Some(ProxySetting::Enabled { http, https, auth }) => {
|
||||
Some(ProxySetting::Enabled {
|
||||
http,
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
}) if !disabled => {
|
||||
debug!("Using proxy http={http} https={https}");
|
||||
let mut proxy = Proxy::custom(move |url| {
|
||||
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
|
||||
@@ -143,12 +156,15 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
None => {} // Nothing to do for this one, as it is the default
|
||||
_ => {} // Nothing to do for this one, as it is the default
|
||||
}
|
||||
|
||||
// Add cookie store if specified
|
||||
let maybe_cookie_manager = match cookie_jar.clone() {
|
||||
Some(cj) => {
|
||||
Some(CookieJar { id, .. }) => {
|
||||
// NOTE: WE need to refetch the cookie jar because a chained request might have
|
||||
// updated cookies when we rendered the request.
|
||||
let cj = window.db().get_cookie_jar(&id)?;
|
||||
// HACK: Can't construct Cookie without serde, so we have to do this
|
||||
let cookies = cj
|
||||
.cookies
|
||||
@@ -187,19 +203,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
query_params.push((p.name, p.value));
|
||||
}
|
||||
|
||||
let uri = match Uri::from_str(url_string.as_str()) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&app_handle,
|
||||
&*response.lock().await,
|
||||
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
||||
&update_source,
|
||||
));
|
||||
}
|
||||
};
|
||||
// Yes, we're parsing both URI and URL because they could return different errors
|
||||
let url = match Url::from_str(uri.to_string().as_str()) {
|
||||
let url = match Url::from_str(&url_string) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
@@ -262,7 +266,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
}
|
||||
|
||||
let request_body = request.body.clone();
|
||||
if let Some(body_type) = &request.body_type {
|
||||
if let Some(body_type) = &request.body_type.clone() {
|
||||
if body_type == "graphql" {
|
||||
let query = get_str_h(&request_body, "query");
|
||||
let variables = get_str_h(&request_body, "variables");
|
||||
@@ -383,7 +387,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
};
|
||||
}
|
||||
|
||||
// Set file path if it is not empty
|
||||
// Set a file path if it is not empty
|
||||
if !file_path.is_empty() {
|
||||
let filename = PathBuf::from(file_path)
|
||||
.file_name()
|
||||
@@ -406,6 +410,15 @@ pub async fn send_http_request<R: Runtime>(
|
||||
} else {
|
||||
warn!("Unsupported body type: {}", body_type);
|
||||
}
|
||||
} else {
|
||||
// No body set
|
||||
let method = request.method.to_ascii_lowercase();
|
||||
let is_body_method = method == "post" || method == "put" || method == "patch";
|
||||
// Add Content-Length for methods that commonly accept a body because some servers
|
||||
// will error if they don't receive it.
|
||||
if is_body_method && !headers.contains_key("content-length") {
|
||||
headers.insert("Content-Length", HeaderValue::from_static("0"));
|
||||
}
|
||||
}
|
||||
|
||||
// Add headers last, because previous steps may modify them
|
||||
@@ -424,43 +437,52 @@ pub async fn send_http_request<R: Runtime>(
|
||||
}
|
||||
};
|
||||
|
||||
// Apply authentication
|
||||
|
||||
if let Some(auth_name) = request.authentication_type.to_owned() {
|
||||
let req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request.id)),
|
||||
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
|
||||
match request.authentication_type {
|
||||
None => {
|
||||
// No authentication found. Not even inherited
|
||||
}
|
||||
Some(authentication_type) if authentication_type == "none" => {
|
||||
// Explicitly no authentication
|
||||
}
|
||||
Some(authentication_type) => {
|
||||
let req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request.id)),
|
||||
values: serde_json::from_value(
|
||||
serde_json::to_value(&request.authentication).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
url: sendable_req.url().to_string(),
|
||||
method: sendable_req.method().to_string(),
|
||||
headers: sendable_req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(name, value)| HttpHeader {
|
||||
name: name.to_string(),
|
||||
value: value.to_str().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let auth_result = plugin_manager.call_http_authentication(&window, &auth_name, req).await;
|
||||
let plugin_result = match auth_result {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&app_handle,
|
||||
&*response.lock().await,
|
||||
e.to_string(),
|
||||
&update_source,
|
||||
));
|
||||
}
|
||||
};
|
||||
url: sendable_req.url().to_string(),
|
||||
method: sendable_req.method().to_string(),
|
||||
headers: sendable_req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(name, value)| HttpHeader {
|
||||
name: name.to_string(),
|
||||
value: value.to_str().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let auth_result =
|
||||
plugin_manager.call_http_authentication(&window, &authentication_type, req).await;
|
||||
let plugin_result = match auth_result {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&app_handle,
|
||||
&*response.lock().await,
|
||||
e.to_string(),
|
||||
&update_source,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let headers = sendable_req.headers_mut();
|
||||
for header in plugin_result.set_headers {
|
||||
headers.insert(
|
||||
HeaderName::from_str(&header.name).unwrap(),
|
||||
HeaderValue::from_str(&header.value).unwrap(),
|
||||
);
|
||||
let headers = sendable_req.headers_mut();
|
||||
for header in plugin_result.set_headers {
|
||||
headers.insert(
|
||||
HeaderName::from_str(&header.name).unwrap(),
|
||||
HeaderValue::from_str(&header.value).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,8 +498,10 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let raw_response = tokio::select! {
|
||||
Ok(r) = resp_rx => r,
|
||||
_ = cancelled_rx.changed() => {
|
||||
debug!("Request cancelled");
|
||||
return Ok(response_err(&app_handle, &*response.lock().await, "Request was cancelled".to_string(), &update_source));
|
||||
let mut r = response.lock().await;
|
||||
r.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||
r.elapsed = start.elapsed().as_millis() as i32;
|
||||
return Ok(response_err(&app_handle, &r, "Request was cancelled".to_string(), &update_source));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -506,6 +530,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let mut r = response.lock().await;
|
||||
r.body_path = Some(body_path.to_str().unwrap().to_string());
|
||||
r.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||
r.elapsed = start.elapsed().as_millis() as i32;
|
||||
r.status = v.status().as_u16() as i32;
|
||||
r.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||
r.headers = response_headers
|
||||
@@ -577,7 +602,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
// Set final content length
|
||||
// Set the final content length
|
||||
{
|
||||
let mut r = response.lock().await;
|
||||
r.content_length = match content_length {
|
||||
@@ -643,6 +668,8 @@ pub async fn send_http_request<R: Runtime>(
|
||||
match app_handle.with_db(|c| c.get_http_response(&response_id)) {
|
||||
Ok(mut r) => {
|
||||
r.state = HttpResponseState::Closed;
|
||||
r.elapsed = start.elapsed().as_millis() as i32;
|
||||
r.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||
app_handle.db().update_http_response_if_id(&r, &UpdateSource::from_window(window))
|
||||
.expect("Failed to update response")
|
||||
},
|
||||
@@ -654,6 +681,23 @@ pub async fn send_http_request<R: Runtime>(
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &HttpRequest,
|
||||
) -> Result<HttpRequest> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication) =
|
||||
window.db().resolve_auth_for_http_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let headers = window.db().resolve_headers_for_http_request(request)?;
|
||||
new_request.headers = headers;
|
||||
|
||||
Ok(new_request)
|
||||
}
|
||||
|
||||
fn ensure_proto(url_str: &str) -> String {
|
||||
if url_str.starts_with("http://") || url_str.starts_with("https://") {
|
||||
return url_str.to_string();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
extern crate core;
|
||||
use crate::encoding::read_response_body;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::grpc::metadata_to_map;
|
||||
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
|
||||
use crate::http_request::send_http_request;
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_grpc_request, render_template};
|
||||
@@ -9,7 +9,7 @@ use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||
use crate::uri_scheme::handle_uri_scheme;
|
||||
use error::Result as YaakResult;
|
||||
use eventsource_client::{EventParser, SSE};
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::path::PathBuf;
|
||||
@@ -38,9 +38,9 @@ use yaak_models::util::{
|
||||
BatchUpsertResult, UpdateSource, get_workspace_export_resources, maybe_gen_id, maybe_gen_id_opt,
|
||||
};
|
||||
use yaak_plugins::events::{
|
||||
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
|
||||
BootResponse, CallHttpRequestActionRequest, FilterResponse,
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, HttpHeader, InternalEvent,
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
|
||||
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
|
||||
};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
@@ -151,10 +151,13 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
None => None,
|
||||
};
|
||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
|
||||
|
||||
let base_environment =
|
||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
||||
|
||||
let req = render_grpc_request(
|
||||
&unrendered_request,
|
||||
&resolved_request,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
&PluginTemplateCallback::new(
|
||||
@@ -166,6 +169,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
.await?;
|
||||
|
||||
let uri = safe_uri(&req.url);
|
||||
let metadata = build_metadata(&window, &req).await?;
|
||||
|
||||
Ok(grpc_handle
|
||||
.lock()
|
||||
@@ -174,6 +178,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
&req.id,
|
||||
&uri,
|
||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||
&metadata,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| GenericError(e.to_string()))?)
|
||||
@@ -186,7 +191,6 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
proto_files: Vec<String>,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
) -> YaakResult<String> {
|
||||
let environment = match environment_id {
|
||||
@@ -194,10 +198,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
None => None,
|
||||
};
|
||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
|
||||
let base_environment =
|
||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
||||
|
||||
let request = render_grpc_request(
|
||||
&unrendered_request,
|
||||
&resolved_request,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
&PluginTemplateCallback::new(
|
||||
@@ -208,42 +214,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut metadata = BTreeMap::new();
|
||||
|
||||
// Add the rest of metadata
|
||||
for h in request.clone().metadata {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !h.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata.insert(h.name, h.value);
|
||||
}
|
||||
|
||||
if let Some(auth_name) = request.authentication_type.clone() {
|
||||
let auth = request.authentication.clone();
|
||||
let plugin_req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request_id.to_string())),
|
||||
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
||||
method: "POST".to_string(),
|
||||
url: request.url.clone(),
|
||||
headers: metadata
|
||||
.iter()
|
||||
.map(|(name, value)| HttpHeader {
|
||||
name: name.to_string(),
|
||||
value: value.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let plugin_result =
|
||||
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
|
||||
for header in plugin_result.set_headers {
|
||||
metadata.insert(header.name, header.value);
|
||||
}
|
||||
}
|
||||
let metadata = build_metadata(&window, &request).await?;
|
||||
|
||||
let conn = app_handle.db().upsert_grpc_connection(
|
||||
&GrpcConnection {
|
||||
@@ -291,6 +262,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&request.clone().id,
|
||||
uri.as_str(),
|
||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||
&metadata,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -448,7 +420,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
|
||||
(true, true) => (
|
||||
Some(
|
||||
connection.streaming(&service, &method, in_msg_stream, metadata).await,
|
||||
connection.streaming(&service, &method, in_msg_stream, &metadata).await,
|
||||
),
|
||||
None,
|
||||
),
|
||||
@@ -456,16 +428,16 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
None,
|
||||
Some(
|
||||
connection
|
||||
.client_streaming(&service, &method, in_msg_stream, metadata)
|
||||
.client_streaming(&service, &method, in_msg_stream, &metadata)
|
||||
.await,
|
||||
),
|
||||
),
|
||||
(false, true) => (
|
||||
Some(connection.server_streaming(&service, &method, &msg, metadata).await),
|
||||
Some(connection.server_streaming(&service, &method, &msg, &metadata).await),
|
||||
None,
|
||||
),
|
||||
(false, false) => {
|
||||
(None, Some(connection.unary(&service, &method, &msg, metadata).await))
|
||||
(None, Some(connection.unary(&service, &method, &msg, &metadata).await))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -831,7 +803,6 @@ async fn cmd_import_data<R: Runtime>(
|
||||
.map(|mut v| {
|
||||
v.id = maybe_gen_id::<Environment>(v.id.as_str(), &mut id_map);
|
||||
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
|
||||
v.environment_id = maybe_gen_id_opt::<Environment>(v.environment_id, &mut id_map);
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
@@ -880,6 +851,8 @@ async fn cmd_import_data<R: Runtime>(
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Importing data");
|
||||
|
||||
let upserted = app_handle.with_tx(|tx| {
|
||||
tx.batch_upsert(
|
||||
workspaces,
|
||||
@@ -949,10 +922,10 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
request_id: &str,
|
||||
model_id: &str,
|
||||
) -> YaakResult<()> {
|
||||
Ok(plugin_manager
|
||||
.call_http_authentication_action(&window, auth_name, action_index, values, request_id)
|
||||
.call_http_authentication_action(&window, auth_name, action_index, values, model_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -983,10 +956,10 @@ async fn cmd_export_data<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
export_path: &str,
|
||||
workspace_ids: Vec<&str>,
|
||||
include_environments: bool,
|
||||
include_private_environments: bool,
|
||||
) -> YaakResult<()> {
|
||||
let export_data =
|
||||
get_workspace_export_resources(&app_handle, workspace_ids, include_environments).await?;
|
||||
get_workspace_export_resources(&app_handle, workspace_ids, include_private_environments)?;
|
||||
let f = File::options()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
||||
@@ -8,6 +8,7 @@ use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
use yaak_license::{LicenseCheckStatus, check_license};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
@@ -70,6 +71,13 @@ impl YaakNotifier {
|
||||
|
||||
self.last_check = SystemTime::now();
|
||||
|
||||
let license_check = match check_license(window).await? {
|
||||
LicenseCheckStatus::PersonalUse { .. } => "personal".to_string(),
|
||||
LicenseCheckStatus::CommercialUse => "commercial".to_string(),
|
||||
LicenseCheckStatus::InvalidLicense => "invalid_license".to_string(),
|
||||
LicenseCheckStatus::Trialing { .. } => "trialing".to_string(),
|
||||
};
|
||||
let settings = window.db().get_settings();
|
||||
let num_launches = get_num_launches(app_handle).await;
|
||||
let info = app_handle.package_info().clone();
|
||||
let req = reqwest::Client::default()
|
||||
@@ -77,6 +85,8 @@ impl YaakNotifier {
|
||||
.query(&[
|
||||
("version", info.version.to_string().as_str()),
|
||||
("launches", num_launches.to_string().as_str()),
|
||||
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()),
|
||||
("license", &license_check),
|
||||
("platform", get_os()),
|
||||
]);
|
||||
let resp = req.send().await?;
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
workspace_from_window,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cookie::Cookie;
|
||||
use log::warn;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
@@ -13,10 +14,11 @@ use yaak_models::models::{HttpResponse, Plugin};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{
|
||||
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
|
||||
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse,
|
||||
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
|
||||
PluginWindowContext, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse,
|
||||
ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent,
|
||||
ListCookieNamesResponse, PluginWindowContext, RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
|
||||
WindowNavigateEvent,
|
||||
};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::plugin_handle::PluginHandle;
|
||||
@@ -269,6 +271,33 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
|
||||
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
|
||||
}
|
||||
InternalEventPayload::ListCookieNamesRequest(_req) => {
|
||||
let window = get_window_from_window_context(app_handle, &window_context)
|
||||
.expect("Failed to find window for listing cookies");
|
||||
let names = match cookie_jar_from_window(&window) {
|
||||
None => Vec::new(),
|
||||
Some(j) => j
|
||||
.cookies
|
||||
.into_iter()
|
||||
.filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
|
||||
.collect(),
|
||||
};
|
||||
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse { names }))
|
||||
}
|
||||
InternalEventPayload::GetCookieValueRequest(req) => {
|
||||
let window = get_window_from_window_context(app_handle, &window_context)
|
||||
.expect("Failed to find window for listing cookies");
|
||||
let value = match cookie_jar_from_window(&window) {
|
||||
None => None,
|
||||
Some(j) => j.cookies.into_iter().find_map(|c| match Cookie::parse(c.raw_cookie) {
|
||||
Ok(c) if c.name().to_string().eq(&req.name) => {
|
||||
Some(c.value_trimmed().to_string())
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
};
|
||||
Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value }))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde_json::Value;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use yaak_http::apply_path_placeholders;
|
||||
use yaak_models::models::{
|
||||
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
||||
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
||||
};
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
|
||||
@@ -37,7 +37,7 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
||||
|
||||
let mut metadata = Vec::new();
|
||||
for p in r.metadata.clone() {
|
||||
metadata.push(GrpcMetadataEntry {
|
||||
metadata.push(HttpRequestHeader {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars, cb).await?,
|
||||
value: render(p.value.as_str(), vars, cb).await?,
|
||||
|
||||
@@ -32,6 +32,7 @@ var import_node_fs = require("node:fs");
|
||||
async function getAccessToken(ctx, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
audience,
|
||||
params,
|
||||
grantType,
|
||||
credentialsInBody,
|
||||
@@ -56,6 +57,7 @@ async function getAccessToken(ctx, {
|
||||
]
|
||||
};
|
||||
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
|
||||
if (scope) httpRequest.body.form.push({ name: "audience", value: audience });
|
||||
if (credentialsInBody) {
|
||||
httpRequest.body.form.push({ name: "client_id", value: clientId });
|
||||
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
|
||||
@@ -64,10 +66,10 @@ async function getAccessToken(ctx, {
|
||||
httpRequest.headers.push({ name: "Authorization", value });
|
||||
}
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
const body = resp.bodyPath ? (0, import_node_fs.readFileSync)(resp.bodyPath, "utf8") : "";
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error("Failed to fetch access token with status=" + resp.status);
|
||||
throw new Error("Failed to fetch access token with status=" + resp.status + " and body=" + body);
|
||||
}
|
||||
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
@@ -168,10 +170,10 @@ async function getOrRefreshAccessToken(ctx, contextId, {
|
||||
await deleteToken(ctx, contextId);
|
||||
return null;
|
||||
}
|
||||
const body = resp.bodyPath ? (0, import_node_fs2.readFileSync)(resp.bodyPath, "utf8") : "";
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error("Failed to fetch access token with status=" + resp.status);
|
||||
throw new Error("Failed to refresh access token with status=" + resp.status + " and body=" + body);
|
||||
}
|
||||
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
@@ -201,6 +203,7 @@ async function getAuthorizationCode(ctx, contextId, {
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
pkce
|
||||
}) {
|
||||
@@ -220,6 +223,7 @@ async function getAuthorizationCode(ctx, contextId, {
|
||||
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (audience) authorizationUrl.searchParams.set("audience", audience);
|
||||
if (pkce) {
|
||||
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
|
||||
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
|
||||
@@ -256,6 +260,7 @@ async function getAuthorizationCode(ctx, contextId, {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: "code", value: code },
|
||||
@@ -291,6 +296,7 @@ async function getClientCredentials(ctx, contextId, {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody
|
||||
}) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
@@ -299,6 +305,7 @@ async function getClientCredentials(ctx, contextId, {
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: "client_credentials",
|
||||
accessTokenUrl,
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
@@ -315,38 +322,46 @@ function getImplicit(ctx, contextId, {
|
||||
clientId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state
|
||||
state,
|
||||
audience
|
||||
}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
}
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
|
||||
authorizationUrl.searchParams.set("response_type", "code");
|
||||
authorizationUrl.searchParams.set("response_type", "token");
|
||||
authorizationUrl.searchParams.set("client_id", clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (audience) authorizationUrl.searchParams.set("audience", audience);
|
||||
if (responseType.includes("id_token")) {
|
||||
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
|
||||
}
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
let foundAccessToken = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: "oauth-authorization-url",
|
||||
async onClose() {
|
||||
if (!foundAccessToken) {
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has("error")) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
|
||||
}
|
||||
close();
|
||||
const hash = url.hash.slice(1);
|
||||
const params = new URLSearchParams(hash);
|
||||
const idToken = params.get("id_token");
|
||||
if (idToken) {
|
||||
params.set("access_token", idToken);
|
||||
params.delete("id_token");
|
||||
const accessToken = params.get("access_token");
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
foundAccessToken = true;
|
||||
close();
|
||||
const response = Object.fromEntries(params);
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
@@ -366,6 +381,7 @@ async function getPassword(ctx, contextId, {
|
||||
username,
|
||||
password,
|
||||
credentialsInBody,
|
||||
audience,
|
||||
scope
|
||||
}) {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
@@ -383,6 +399,7 @@ async function getPassword(ctx, contextId, {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
grantType: "password",
|
||||
credentialsInBody,
|
||||
params: [
|
||||
@@ -530,6 +547,12 @@ var plugin = {
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"])
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "audience",
|
||||
label: "Audience",
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "usePkce",
|
||||
@@ -635,6 +658,7 @@ var plugin = {
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
state: stringArgOrNull(values, "state"),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce ? {
|
||||
@@ -650,6 +674,7 @@ var plugin = {
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
responseType: stringArg(values, "responseType"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
state: stringArgOrNull(values, "state")
|
||||
});
|
||||
} else if (grantType === "client_credentials") {
|
||||
@@ -659,6 +684,7 @@ var plugin = {
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
credentialsInBody
|
||||
});
|
||||
} else if (grantType === "password") {
|
||||
@@ -670,6 +696,7 @@ var plugin = {
|
||||
username: stringArg(values, "username"),
|
||||
password: stringArg(values, "password"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
credentialsInBody
|
||||
});
|
||||
} else {
|
||||
|
||||
1287
src-tauri/vendored/plugins/filter-jsonpath/build/index.js
generated
1287
src-tauri/vendored/plugins/filter-jsonpath/build/index.js
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^9.0.0"
|
||||
"jsonpath-plus": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
|
||||
@@ -542,20 +542,23 @@ function pairsToDataParameters(keyedPairs) {
|
||||
}
|
||||
for (const p of pairs) {
|
||||
if (typeof p !== "string") continue;
|
||||
const [name, value] = p.split("=");
|
||||
if (p.startsWith("@")) {
|
||||
dataParameters.push({
|
||||
name: name ?? "",
|
||||
value: "",
|
||||
filePath: p.slice(1),
|
||||
enabled: true
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? "",
|
||||
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "",
|
||||
enabled: true
|
||||
});
|
||||
let params = p.split("&");
|
||||
for (const param of params) {
|
||||
const [name, value] = param.split("=");
|
||||
if (param.startsWith("@")) {
|
||||
dataParameters.push({
|
||||
name: name ?? "",
|
||||
value: "",
|
||||
filePath: param.slice(1),
|
||||
enabled: true
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? "",
|
||||
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "",
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7296,35 +7296,48 @@ __export(src_exports, {
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var import_yaml = __toESM(require_dist());
|
||||
var plugin = {
|
||||
importer: {
|
||||
name: "Insomnia",
|
||||
description: "Import Insomnia workspaces",
|
||||
onImport(_ctx, args) {
|
||||
return convertInsomnia(args.text);
|
||||
}
|
||||
|
||||
// src/common.ts
|
||||
function convertSyntax(variable) {
|
||||
if (!isJSString(variable)) return variable;
|
||||
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}");
|
||||
}
|
||||
function isJSObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
function isJSString(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object String]";
|
||||
}
|
||||
function convertId(id) {
|
||||
if (id.startsWith("GENERATE_ID::")) {
|
||||
return id;
|
||||
}
|
||||
};
|
||||
function convertInsomnia(contents) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (e) {
|
||||
return `GENERATE_ID::${id}`;
|
||||
}
|
||||
function deleteUndefinedAttrs(obj) {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs);
|
||||
} else if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([, v]) => v !== void 0).map(([k, v]) => [k, deleteUndefinedAttrs(v)])
|
||||
);
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
try {
|
||||
parsed = parsed ?? import_yaml.default.parse(contents);
|
||||
} catch (e) {
|
||||
}
|
||||
if (!isJSObject(parsed)) return;
|
||||
if (!Array.isArray(parsed.resources)) return;
|
||||
}
|
||||
|
||||
// src/v4.ts
|
||||
function convertInsomniaV4(parsed) {
|
||||
if (!Array.isArray(parsed.resources)) return null;
|
||||
const resources = {
|
||||
workspaces: [],
|
||||
httpRequests: [],
|
||||
grpcRequests: [],
|
||||
environments: [],
|
||||
folders: []
|
||||
folders: [],
|
||||
grpcRequests: [],
|
||||
httpRequests: [],
|
||||
websocketRequests: [],
|
||||
workspaces: []
|
||||
};
|
||||
const workspacesToImport = parsed.resources.filter(isWorkspace);
|
||||
const workspacesToImport = parsed.resources.filter((r) => isJSObject(r) && r._type === "workspace");
|
||||
for (const w of workspacesToImport) {
|
||||
resources.workspaces.push({
|
||||
id: convertId(w._id),
|
||||
@@ -7335,25 +7348,25 @@ function convertInsomnia(contents) {
|
||||
description: w.description || void 0
|
||||
});
|
||||
const environmentsToImport = parsed.resources.filter(
|
||||
(r) => isEnvironment(r)
|
||||
(r) => isJSObject(r) && r._type === "environment"
|
||||
);
|
||||
resources.environments.push(
|
||||
...environmentsToImport.map((r) => importEnvironment(r, w._id))
|
||||
);
|
||||
const nextFolder = (parentId) => {
|
||||
const children = parsed.resources.filter((r) => r.parentId === parentId);
|
||||
let sortPriority = 0;
|
||||
for (const child of children) {
|
||||
if (isRequestGroup(child)) {
|
||||
if (!isJSObject(child)) continue;
|
||||
if (child._type === "request_group") {
|
||||
resources.folders.push(importFolder(child, w._id));
|
||||
nextFolder(child._id);
|
||||
} else if (isHttpRequest(child)) {
|
||||
} else if (child._type === "request") {
|
||||
resources.httpRequests.push(
|
||||
importHttpRequest(child, w._id, sortPriority++)
|
||||
importHttpRequest(child, w._id)
|
||||
);
|
||||
} else if (isGrpcRequest(child)) {
|
||||
} else if (child._type === "grpc_request") {
|
||||
resources.grpcRequests.push(
|
||||
importGrpcRequest(child, w._id, sortPriority++)
|
||||
importGrpcRequest(child, w._id)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7364,62 +7377,9 @@ function convertInsomnia(contents) {
|
||||
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
return { resources: deleteUndefinedAttrs(resources) };
|
||||
return { resources };
|
||||
}
|
||||
function importEnvironment(e, workspaceId) {
|
||||
return {
|
||||
id: convertId(e._id),
|
||||
createdAt: e.created ? new Date(e.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: e.updated ? new Date(e.updated).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
environmentId: e.parentId === workspaceId ? null : convertId(e.parentId),
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
function importFolder(f, workspaceId) {
|
||||
return {
|
||||
id: convertId(f._id),
|
||||
createdAt: f.created ? new Date(f.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: f.updated ? new Date(f.updated).toISOString().replace("Z", "") : void 0,
|
||||
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || void 0,
|
||||
model: "folder",
|
||||
name: f.name
|
||||
};
|
||||
}
|
||||
function importGrpcRequest(r, workspaceId, sortPriority = 0) {
|
||||
const parts = r.protoMethodName.split("/").filter((p) => p !== "");
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
return {
|
||||
id: convertId(r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: r.updated ? new Date(r.updated).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: "grpc_request",
|
||||
sortPriority,
|
||||
name: r.name,
|
||||
description: r.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? "",
|
||||
metadata: (r.metadata ?? []).map((h) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? ""
|
||||
})).filter(({ name, value }) => name !== "" || value !== "")
|
||||
};
|
||||
}
|
||||
function importHttpRequest(r, workspaceId, sortPriority = 0) {
|
||||
function importHttpRequest(r, workspaceId) {
|
||||
let bodyType = null;
|
||||
let body = {};
|
||||
if (r.body.mimeType === "application/octet-stream") {
|
||||
@@ -7466,13 +7426,13 @@ function importHttpRequest(r, workspaceId, sortPriority = 0) {
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: convertId(r._id),
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: r.updated ? new Date(r.updated).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: "http_request",
|
||||
sortPriority,
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
@@ -7488,47 +7448,309 @@ function importHttpRequest(r, workspaceId, sortPriority = 0) {
|
||||
})).filter(({ name, value }) => name !== "" || value !== "")
|
||||
};
|
||||
}
|
||||
function convertSyntax(variable) {
|
||||
if (!isJSString(variable)) return variable;
|
||||
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}");
|
||||
function importGrpcRequest(r, workspaceId) {
|
||||
const parts = r.protoMethodName.split("/").filter((p) => p !== "");
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
return {
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: "grpc_request",
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? "",
|
||||
metadata: (r.metadata ?? []).map((h) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? ""
|
||||
})).filter(({ name, value }) => name !== "" || value !== "")
|
||||
};
|
||||
}
|
||||
function isWorkspace(obj) {
|
||||
return isJSObject(obj) && obj._type === "workspace";
|
||||
function importFolder(f, workspaceId) {
|
||||
return {
|
||||
id: convertId(f._id),
|
||||
createdAt: f.created ? new Date(f.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: f.modified ? new Date(f.modified).toISOString().replace("Z", "") : void 0,
|
||||
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || void 0,
|
||||
model: "folder",
|
||||
name: f.name
|
||||
};
|
||||
}
|
||||
function isRequestGroup(obj) {
|
||||
return isJSObject(obj) && obj._type === "request_group";
|
||||
function importEnvironment(e, workspaceId, isParent) {
|
||||
return {
|
||||
id: convertId(e._id),
|
||||
createdAt: e.created ? new Date(e.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: e.modified ? new Date(e.modified).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
// @ts-ignore
|
||||
sortPriority: e.metaSortKey,
|
||||
// Will be added to Yaak later
|
||||
base: isParent ?? e.parentId === workspaceId,
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
function isHttpRequest(obj) {
|
||||
return isJSObject(obj) && obj._type === "request";
|
||||
|
||||
// src/v5.ts
|
||||
function convertInsomniaV5(parsed) {
|
||||
if (!Array.isArray(parsed.collection)) return null;
|
||||
const resources = {
|
||||
environments: [],
|
||||
folders: [],
|
||||
grpcRequests: [],
|
||||
httpRequests: [],
|
||||
websocketRequests: [],
|
||||
workspaces: []
|
||||
};
|
||||
const meta = parsed.meta ?? {};
|
||||
resources.workspaces.push({
|
||||
id: convertId(meta.id ?? "collection"),
|
||||
createdAt: meta.created ? new Date(meta.created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace("Z", "") : void 0,
|
||||
model: "workspace",
|
||||
name: parsed.name,
|
||||
description: meta.description || void 0
|
||||
});
|
||||
resources.environments.push(
|
||||
importEnvironment2(parsed.environments, meta.id, true),
|
||||
...(parsed.environments.subEnvironments ?? []).map((r) => importEnvironment2(r, meta.id))
|
||||
);
|
||||
const nextFolder = (children, parentId) => {
|
||||
for (const child of children ?? []) {
|
||||
if (!isJSObject(child)) continue;
|
||||
if (Array.isArray(child.children)) {
|
||||
resources.folders.push(importFolder2(child, meta.id, parentId));
|
||||
nextFolder(child.children, child.meta.id);
|
||||
} else if (child.method) {
|
||||
resources.httpRequests.push(
|
||||
importHttpRequest2(child, meta.id, parentId)
|
||||
);
|
||||
} else if (child.protoFileId) {
|
||||
resources.grpcRequests.push(
|
||||
importGrpcRequest2(child, meta.id, parentId)
|
||||
);
|
||||
} else if (child.url) {
|
||||
resources.websocketRequests.push(
|
||||
importWebsocketRequest(child, meta.id, parentId)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
nextFolder(parsed.collection ?? [], meta.id);
|
||||
resources.httpRequests = resources.httpRequests.filter(Boolean);
|
||||
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
return { resources };
|
||||
}
|
||||
function isGrpcRequest(obj) {
|
||||
return isJSObject(obj) && obj._type === "grpc_request";
|
||||
}
|
||||
function isEnvironment(obj) {
|
||||
return isJSObject(obj) && obj._type === "environment";
|
||||
}
|
||||
function isJSObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
function isJSString(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object String]";
|
||||
}
|
||||
function convertId(id) {
|
||||
if (id.startsWith("GENERATE_ID::")) {
|
||||
return id;
|
||||
function importHttpRequest2(r, workspaceId, parentId) {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
let bodyType = null;
|
||||
let body = {};
|
||||
if (r.body?.mimeType === "application/octet-stream") {
|
||||
bodyType = "binary";
|
||||
body = { filePath: r.body.fileName ?? "" };
|
||||
} else if (r.body?.mimeType === "application/x-www-form-urlencoded") {
|
||||
bodyType = "application/x-www-form-urlencoded";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? ""
|
||||
}))
|
||||
};
|
||||
} else if (r.body?.mimeType === "multipart/form-data") {
|
||||
bodyType = "multipart/form-data";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
file: p.fileName ?? null
|
||||
}))
|
||||
};
|
||||
} else if (r.body?.mimeType === "application/graphql") {
|
||||
bodyType = "graphql";
|
||||
body = { text: convertSyntax(r.body.text ?? "") };
|
||||
} else if (r.body?.mimeType === "application/json") {
|
||||
bodyType = "application/json";
|
||||
body = { text: convertSyntax(r.body.text ?? "") };
|
||||
}
|
||||
return `GENERATE_ID::${id}`;
|
||||
return {
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : void 0,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
model: "http_request",
|
||||
name: r.name,
|
||||
description: r.meta?.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
body,
|
||||
bodyType,
|
||||
method: r.method,
|
||||
...importHeaders(r),
|
||||
...importAuthentication(r)
|
||||
};
|
||||
}
|
||||
function deleteUndefinedAttrs(obj) {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs);
|
||||
} else if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([, v]) => v !== void 0).map(([k, v]) => [k, deleteUndefinedAttrs(v)])
|
||||
);
|
||||
} else {
|
||||
return obj;
|
||||
function importGrpcRequest2(r, workspaceId, parentId) {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
const parts = r.protoMethodName.split("/").filter((p) => p !== "");
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
return {
|
||||
model: "grpc_request",
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : void 0,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
description: r.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? "",
|
||||
metadata: (r.metadata ?? []).map((h) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? ""
|
||||
})).filter(({ name, value }) => name !== "" || value !== "")
|
||||
};
|
||||
}
|
||||
function importWebsocketRequest(r, workspaceId, parentId) {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
return {
|
||||
model: "websocket_request",
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : void 0,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
description: r.description || void 0,
|
||||
url: convertSyntax(r.url),
|
||||
message: r.body?.text ?? "",
|
||||
...importHeaders(r),
|
||||
...importAuthentication(r)
|
||||
};
|
||||
}
|
||||
function importHeaders(r) {
|
||||
const headers = (r.headers ?? []).map((h) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? ""
|
||||
})).filter(({ name, value }) => name !== "" || value !== "");
|
||||
return { headers };
|
||||
}
|
||||
function importAuthentication(r) {
|
||||
let authenticationType = null;
|
||||
let authentication = {};
|
||||
if (r.authentication?.type === "bearer") {
|
||||
authenticationType = "bearer";
|
||||
authentication = {
|
||||
token: convertSyntax(r.authentication.token)
|
||||
};
|
||||
} else if (r.authentication?.type === "basic") {
|
||||
authenticationType = "basic";
|
||||
authentication = {
|
||||
username: convertSyntax(r.authentication.username),
|
||||
password: convertSyntax(r.authentication.password)
|
||||
};
|
||||
}
|
||||
return { authenticationType, authentication };
|
||||
}
|
||||
function importFolder2(f, workspaceId, parentId) {
|
||||
const id = f.meta?.id ?? f._id;
|
||||
const created = f.meta?.created ?? f.created;
|
||||
const updated = f.meta?.modified ?? f.updated;
|
||||
const sortKey = f.meta?.sortKey ?? f.sortKey;
|
||||
return {
|
||||
model: "folder",
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : void 0,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || void 0,
|
||||
name: f.name
|
||||
};
|
||||
}
|
||||
function importEnvironment2(e, workspaceId, isParent) {
|
||||
const id = e.meta?.id ?? e._id;
|
||||
const created = e.meta?.created ?? e.created;
|
||||
const updated = e.meta?.modified ?? e.updated;
|
||||
const sortKey = e.meta?.sortKey ?? e.sortKey;
|
||||
return {
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : void 0,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : void 0,
|
||||
workspaceId: convertId(workspaceId),
|
||||
public: !e.isPrivate,
|
||||
// @ts-ignore
|
||||
sortPriority: sortKey,
|
||||
// Will be added to Yaak later
|
||||
base: isParent ?? e.parentId === workspaceId,
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var plugin = {
|
||||
importer: {
|
||||
name: "Insomnia",
|
||||
description: "Import Insomnia workspaces",
|
||||
async onImport(_ctx, args) {
|
||||
return convertInsomnia(args.text);
|
||||
}
|
||||
}
|
||||
};
|
||||
function convertInsomnia(contents) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
parsed = parsed ?? import_yaml.default.parse(contents);
|
||||
} catch (e) {
|
||||
}
|
||||
if (!isJSObject(parsed)) return null;
|
||||
const result = convertInsomniaV5(parsed) ?? convertInsomniaV4(parsed);
|
||||
return deleteUndefinedAttrs(result);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
|
||||
@@ -69,6 +69,12 @@ function migrateImport(contents) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const environment of parsed.resources.environments ?? []) {
|
||||
if ("environmentId" in environment) {
|
||||
environment.base = environment.environmentId == null;
|
||||
delete environment.environmentId;
|
||||
}
|
||||
}
|
||||
return { resources: parsed.resources };
|
||||
}
|
||||
function isJSObject(obj) {
|
||||
|
||||
47
src-tauri/vendored/plugins/template-function-cookie/build/index.js
generated
Normal file
47
src-tauri/vendored/plugins/template-function-cookie/build/index.js
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var plugin = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: "cookie.value",
|
||||
description: "Read the value of a cookie in the jar, by name",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "cookie_name",
|
||||
label: "Cookie Name"
|
||||
}
|
||||
],
|
||||
async onRender(ctx, args) {
|
||||
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
9
src-tauri/vendored/plugins/template-function-cookie/package.json
generated
Normal file
9
src-tauri/vendored/plugins/template-function-cookie/package.json
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-cookie",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
}
|
||||
}
|
||||
69
src-tauri/vendored/plugins/template-function-encode/build/index.js
generated
Normal file
69
src-tauri/vendored/plugins/template-function-encode/build/index.js
generated
Normal file
@@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var plugin = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: "base64.encode",
|
||||
description: "Encode a value to base64",
|
||||
args: [{ label: "Plain Text", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx, args) {
|
||||
return Buffer.from(args.values.value ?? "").toString("base64");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "base64.decode",
|
||||
description: "Decode a value from base64",
|
||||
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx, args) {
|
||||
return Buffer.from(args.values.value ?? "", "base64").toString("utf-8");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "url.encode",
|
||||
description: "Encode a value for use in a URL (percent-encoding)",
|
||||
args: [{ label: "Plain Text", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx, args) {
|
||||
return encodeURIComponent(args.values.value ?? "");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "url.decode",
|
||||
description: "Decode a percent-encoded URL value",
|
||||
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx, args) {
|
||||
try {
|
||||
return decodeURIComponent(args.values.value ?? "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
9
src-tauri/vendored/plugins/template-function-encode/package.json
generated
Normal file
9
src-tauri/vendored/plugins/template-function-encode/package.json
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-encode",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
}
|
||||
}
|
||||
@@ -25,24 +25,76 @@ __export(src_exports, {
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var import_node_crypto = require("node:crypto");
|
||||
var algorithms = ["md5", "sha1", "sha256", "sha512"];
|
||||
var plugin = {
|
||||
templateFunctions: algorithms.map((algorithm) => ({
|
||||
name: `hash.${algorithm}`,
|
||||
description: "Hash a value to its hexidecimal representation",
|
||||
args: [
|
||||
{
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: "input text",
|
||||
type: "text"
|
||||
}
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
if (!args.values.input) return "";
|
||||
return (0, import_node_crypto.createHash)(algorithm).update(args.values.input, "utf-8").digest("hex");
|
||||
var encodings = ["base64", "hex"];
|
||||
var hashFunctions = algorithms.map((algorithm) => ({
|
||||
name: `hash.${algorithm}`,
|
||||
description: "Hash a value to its hexidecimal representation",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: "input text",
|
||||
multiLine: true
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
label: "Encoding",
|
||||
defaultValue: "base64",
|
||||
options: encodings.map((encoding) => ({
|
||||
label: capitalize(encoding),
|
||||
value: encoding
|
||||
}))
|
||||
}
|
||||
}))
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
const input = String(args.values.input);
|
||||
const encoding = String(args.values.encoding);
|
||||
return (0, import_node_crypto.createHash)(algorithm).update(input, "utf-8").digest(encoding);
|
||||
}
|
||||
}));
|
||||
var hmacFunctions = algorithms.map((algorithm) => ({
|
||||
name: `hmac.${algorithm}`,
|
||||
description: "Compute the HMAC of a value",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: "input text",
|
||||
multiLine: true
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
label: "Key",
|
||||
password: true
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
label: "Encoding",
|
||||
defaultValue: "base64",
|
||||
options: encodings.map((encoding) => ({
|
||||
value: encoding,
|
||||
label: capitalize(encoding)
|
||||
}))
|
||||
}
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
const input = String(args.values.input);
|
||||
const key = String(args.values.key);
|
||||
const encoding = String(args.values.encoding);
|
||||
return (0, import_node_crypto.createHmac)(algorithm, key, {}).update(input).digest(encoding);
|
||||
}
|
||||
}));
|
||||
var plugin = {
|
||||
templateFunctions: [...hashFunctions, ...hmacFunctions]
|
||||
};
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
|
||||
1769
src-tauri/vendored/plugins/template-function-json/build/index.js
generated
Normal file
1769
src-tauri/vendored/plugins/template-function-json/build/index.js
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
src-tauri/vendored/plugins/template-function-json/package.json
generated
Executable file
15
src-tauri/vendored/plugins/template-function-json/package.json
generated
Executable file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-json",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
}
|
||||
}
|
||||
52
src-tauri/vendored/plugins/template-function-regex/build/index.js
generated
Normal file
52
src-tauri/vendored/plugins/template-function-regex/build/index.js
generated
Normal file
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var plugin = {
|
||||
templateFunctions: [{
|
||||
name: "regex.match",
|
||||
description: "Extract",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "regex",
|
||||
label: "Regular Expression",
|
||||
placeholder: "^w+=(?<value>w*)$",
|
||||
defaultValue: "^(.*)$",
|
||||
description: "A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values."
|
||||
},
|
||||
{ type: "text", name: "input", label: "Input Text", multiLine: true }
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
if (!args.values.regex) return "";
|
||||
const regex = new RegExp(String(args.values.regex));
|
||||
const match = args.values.input?.match(regex);
|
||||
return match?.groups ? Object.values(match.groups)[0] ?? "" : match?.[1] ?? match?.[0] ?? "";
|
||||
}
|
||||
}]
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
9
src-tauri/vendored/plugins/template-function-regex/package.json
generated
Normal file
9
src-tauri/vendored/plugins/template-function-regex/package.json
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-regex",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
}
|
||||
}
|
||||
417
src-tauri/vendored/plugins/template-function-uuid/build/index.js
generated
Normal file
417
src-tauri/vendored/plugins/template-function-uuid/build/index.js
generated
Normal file
@@ -0,0 +1,417 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
|
||||
// node_modules/uuid/dist/esm/regex.js
|
||||
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
|
||||
|
||||
// node_modules/uuid/dist/esm/validate.js
|
||||
function validate(uuid) {
|
||||
return typeof uuid === "string" && regex_default.test(uuid);
|
||||
}
|
||||
var validate_default = validate;
|
||||
|
||||
// node_modules/uuid/dist/esm/parse.js
|
||||
function parse(uuid) {
|
||||
if (!validate_default(uuid)) {
|
||||
throw TypeError("Invalid UUID");
|
||||
}
|
||||
let v;
|
||||
return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
|
||||
}
|
||||
var parse_default = parse;
|
||||
|
||||
// node_modules/uuid/dist/esm/stringify.js
|
||||
var byteToHex = [];
|
||||
for (let i = 0; i < 256; ++i) {
|
||||
byteToHex.push((i + 256).toString(16).slice(1));
|
||||
}
|
||||
function unsafeStringify(arr, offset = 0) {
|
||||
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
||||
}
|
||||
|
||||
// node_modules/uuid/dist/esm/rng.js
|
||||
var import_crypto = require("crypto");
|
||||
var rnds8Pool = new Uint8Array(256);
|
||||
var poolPtr = rnds8Pool.length;
|
||||
function rng() {
|
||||
if (poolPtr > rnds8Pool.length - 16) {
|
||||
(0, import_crypto.randomFillSync)(rnds8Pool);
|
||||
poolPtr = 0;
|
||||
}
|
||||
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
||||
}
|
||||
|
||||
// node_modules/uuid/dist/esm/v1.js
|
||||
var _state = {};
|
||||
function v1(options, buf, offset) {
|
||||
let bytes;
|
||||
const isV6 = options?._v6 ?? false;
|
||||
if (options) {
|
||||
const optionsKeys = Object.keys(options);
|
||||
if (optionsKeys.length === 1 && optionsKeys[0] === "_v6") {
|
||||
options = void 0;
|
||||
}
|
||||
}
|
||||
if (options) {
|
||||
bytes = v1Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.nsecs, options.clockseq, options.node, buf, offset);
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const rnds = rng();
|
||||
updateV1State(_state, now, rnds);
|
||||
bytes = v1Bytes(rnds, _state.msecs, _state.nsecs, isV6 ? void 0 : _state.clockseq, isV6 ? void 0 : _state.node, buf, offset);
|
||||
}
|
||||
return buf ?? unsafeStringify(bytes);
|
||||
}
|
||||
function updateV1State(state, now, rnds) {
|
||||
state.msecs ??= -Infinity;
|
||||
state.nsecs ??= 0;
|
||||
if (now === state.msecs) {
|
||||
state.nsecs++;
|
||||
if (state.nsecs >= 1e4) {
|
||||
state.node = void 0;
|
||||
state.nsecs = 0;
|
||||
}
|
||||
} else if (now > state.msecs) {
|
||||
state.nsecs = 0;
|
||||
} else if (now < state.msecs) {
|
||||
state.node = void 0;
|
||||
}
|
||||
if (!state.node) {
|
||||
state.node = rnds.slice(10, 16);
|
||||
state.node[0] |= 1;
|
||||
state.clockseq = (rnds[8] << 8 | rnds[9]) & 16383;
|
||||
}
|
||||
state.msecs = now;
|
||||
return state;
|
||||
}
|
||||
function v1Bytes(rnds, msecs, nsecs, clockseq, node, buf, offset = 0) {
|
||||
if (rnds.length < 16) {
|
||||
throw new Error("Random bytes length must be >= 16");
|
||||
}
|
||||
if (!buf) {
|
||||
buf = new Uint8Array(16);
|
||||
offset = 0;
|
||||
} else {
|
||||
if (offset < 0 || offset + 16 > buf.length) {
|
||||
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
||||
}
|
||||
}
|
||||
msecs ??= Date.now();
|
||||
nsecs ??= 0;
|
||||
clockseq ??= (rnds[8] << 8 | rnds[9]) & 16383;
|
||||
node ??= rnds.slice(10, 16);
|
||||
msecs += 122192928e5;
|
||||
const tl = ((msecs & 268435455) * 1e4 + nsecs) % 4294967296;
|
||||
buf[offset++] = tl >>> 24 & 255;
|
||||
buf[offset++] = tl >>> 16 & 255;
|
||||
buf[offset++] = tl >>> 8 & 255;
|
||||
buf[offset++] = tl & 255;
|
||||
const tmh = msecs / 4294967296 * 1e4 & 268435455;
|
||||
buf[offset++] = tmh >>> 8 & 255;
|
||||
buf[offset++] = tmh & 255;
|
||||
buf[offset++] = tmh >>> 24 & 15 | 16;
|
||||
buf[offset++] = tmh >>> 16 & 255;
|
||||
buf[offset++] = clockseq >>> 8 | 128;
|
||||
buf[offset++] = clockseq & 255;
|
||||
for (let n = 0; n < 6; ++n) {
|
||||
buf[offset++] = node[n];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
var v1_default = v1;
|
||||
|
||||
// node_modules/uuid/dist/esm/v1ToV6.js
|
||||
function v1ToV6(uuid) {
|
||||
const v1Bytes2 = typeof uuid === "string" ? parse_default(uuid) : uuid;
|
||||
const v6Bytes = _v1ToV6(v1Bytes2);
|
||||
return typeof uuid === "string" ? unsafeStringify(v6Bytes) : v6Bytes;
|
||||
}
|
||||
function _v1ToV6(v1Bytes2) {
|
||||
return Uint8Array.of((v1Bytes2[6] & 15) << 4 | v1Bytes2[7] >> 4 & 15, (v1Bytes2[7] & 15) << 4 | (v1Bytes2[4] & 240) >> 4, (v1Bytes2[4] & 15) << 4 | (v1Bytes2[5] & 240) >> 4, (v1Bytes2[5] & 15) << 4 | (v1Bytes2[0] & 240) >> 4, (v1Bytes2[0] & 15) << 4 | (v1Bytes2[1] & 240) >> 4, (v1Bytes2[1] & 15) << 4 | (v1Bytes2[2] & 240) >> 4, 96 | v1Bytes2[2] & 15, v1Bytes2[3], v1Bytes2[8], v1Bytes2[9], v1Bytes2[10], v1Bytes2[11], v1Bytes2[12], v1Bytes2[13], v1Bytes2[14], v1Bytes2[15]);
|
||||
}
|
||||
|
||||
// node_modules/uuid/dist/esm/md5.js
|
||||
var import_crypto2 = require("crypto");
|
||||
function md5(bytes) {
|
||||
if (Array.isArray(bytes)) {
|
||||
bytes = Buffer.from(bytes);
|
||||
} else if (typeof bytes === "string") {
|
||||
bytes = Buffer.from(bytes, "utf8");
|
||||
}
|
||||
return (0, import_crypto2.createHash)("md5").update(bytes).digest();
|
||||
}
|
||||
var md5_default = md5;
|
||||
|
||||
// node_modules/uuid/dist/esm/v35.js
|
||||
function stringToBytes(str) {
|
||||
str = unescape(encodeURIComponent(str));
|
||||
const bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
var DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
||||
var URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
|
||||
function v35(version, hash, value, namespace, buf, offset) {
|
||||
const valueBytes = typeof value === "string" ? stringToBytes(value) : value;
|
||||
const namespaceBytes = typeof namespace === "string" ? parse_default(namespace) : namespace;
|
||||
if (typeof namespace === "string") {
|
||||
namespace = parse_default(namespace);
|
||||
}
|
||||
if (namespace?.length !== 16) {
|
||||
throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");
|
||||
}
|
||||
let bytes = new Uint8Array(16 + valueBytes.length);
|
||||
bytes.set(namespaceBytes);
|
||||
bytes.set(valueBytes, namespaceBytes.length);
|
||||
bytes = hash(bytes);
|
||||
bytes[6] = bytes[6] & 15 | version;
|
||||
bytes[8] = bytes[8] & 63 | 128;
|
||||
if (buf) {
|
||||
offset = offset || 0;
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
buf[offset + i] = bytes[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
return unsafeStringify(bytes);
|
||||
}
|
||||
|
||||
// node_modules/uuid/dist/esm/v3.js
|
||||
function v3(value, namespace, buf, offset) {
|
||||
return v35(48, md5_default, value, namespace, buf, offset);
|
||||
}
|
||||
v3.DNS = DNS;
|
||||
v3.URL = URL;
|
||||
var v3_default = v3;
|
||||
|
||||
// node_modules/uuid/dist/esm/native.js
|
||||
var import_crypto3 = require("crypto");
|
||||
var native_default = { randomUUID: import_crypto3.randomUUID };
|
||||
|
||||
// node_modules/uuid/dist/esm/v4.js
|
||||
function v4(options, buf, offset) {
|
||||
if (native_default.randomUUID && !buf && !options) {
|
||||
return native_default.randomUUID();
|
||||
}
|
||||
options = options || {};
|
||||
const rnds = options.random ?? options.rng?.() ?? rng();
|
||||
if (rnds.length < 16) {
|
||||
throw new Error("Random bytes length must be >= 16");
|
||||
}
|
||||
rnds[6] = rnds[6] & 15 | 64;
|
||||
rnds[8] = rnds[8] & 63 | 128;
|
||||
if (buf) {
|
||||
offset = offset || 0;
|
||||
if (offset < 0 || offset + 16 > buf.length) {
|
||||
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
||||
}
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
buf[offset + i] = rnds[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
return unsafeStringify(rnds);
|
||||
}
|
||||
var v4_default = v4;
|
||||
|
||||
// node_modules/uuid/dist/esm/sha1.js
|
||||
var import_crypto4 = require("crypto");
|
||||
function sha1(bytes) {
|
||||
if (Array.isArray(bytes)) {
|
||||
bytes = Buffer.from(bytes);
|
||||
} else if (typeof bytes === "string") {
|
||||
bytes = Buffer.from(bytes, "utf8");
|
||||
}
|
||||
return (0, import_crypto4.createHash)("sha1").update(bytes).digest();
|
||||
}
|
||||
var sha1_default = sha1;
|
||||
|
||||
// node_modules/uuid/dist/esm/v5.js
|
||||
function v5(value, namespace, buf, offset) {
|
||||
return v35(80, sha1_default, value, namespace, buf, offset);
|
||||
}
|
||||
v5.DNS = DNS;
|
||||
v5.URL = URL;
|
||||
var v5_default = v5;
|
||||
|
||||
// node_modules/uuid/dist/esm/v6.js
|
||||
function v6(options, buf, offset) {
|
||||
options ??= {};
|
||||
offset ??= 0;
|
||||
let bytes = v1_default({ ...options, _v6: true }, new Uint8Array(16));
|
||||
bytes = v1ToV6(bytes);
|
||||
if (buf) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
buf[offset + i] = bytes[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
return unsafeStringify(bytes);
|
||||
}
|
||||
var v6_default = v6;
|
||||
|
||||
// node_modules/uuid/dist/esm/v7.js
|
||||
var _state2 = {};
|
||||
function v7(options, buf, offset) {
|
||||
let bytes;
|
||||
if (options) {
|
||||
bytes = v7Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.seq, buf, offset);
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const rnds = rng();
|
||||
updateV7State(_state2, now, rnds);
|
||||
bytes = v7Bytes(rnds, _state2.msecs, _state2.seq, buf, offset);
|
||||
}
|
||||
return buf ?? unsafeStringify(bytes);
|
||||
}
|
||||
function updateV7State(state, now, rnds) {
|
||||
state.msecs ??= -Infinity;
|
||||
state.seq ??= 0;
|
||||
if (now > state.msecs) {
|
||||
state.seq = rnds[6] << 23 | rnds[7] << 16 | rnds[8] << 8 | rnds[9];
|
||||
state.msecs = now;
|
||||
} else {
|
||||
state.seq = state.seq + 1 | 0;
|
||||
if (state.seq === 0) {
|
||||
state.msecs++;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
function v7Bytes(rnds, msecs, seq, buf, offset = 0) {
|
||||
if (rnds.length < 16) {
|
||||
throw new Error("Random bytes length must be >= 16");
|
||||
}
|
||||
if (!buf) {
|
||||
buf = new Uint8Array(16);
|
||||
offset = 0;
|
||||
} else {
|
||||
if (offset < 0 || offset + 16 > buf.length) {
|
||||
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
||||
}
|
||||
}
|
||||
msecs ??= Date.now();
|
||||
seq ??= rnds[6] * 127 << 24 | rnds[7] << 16 | rnds[8] << 8 | rnds[9];
|
||||
buf[offset++] = msecs / 1099511627776 & 255;
|
||||
buf[offset++] = msecs / 4294967296 & 255;
|
||||
buf[offset++] = msecs / 16777216 & 255;
|
||||
buf[offset++] = msecs / 65536 & 255;
|
||||
buf[offset++] = msecs / 256 & 255;
|
||||
buf[offset++] = msecs & 255;
|
||||
buf[offset++] = 112 | seq >>> 28 & 15;
|
||||
buf[offset++] = seq >>> 20 & 255;
|
||||
buf[offset++] = 128 | seq >>> 14 & 63;
|
||||
buf[offset++] = seq >>> 6 & 255;
|
||||
buf[offset++] = seq << 2 & 255 | rnds[10] & 3;
|
||||
buf[offset++] = rnds[11];
|
||||
buf[offset++] = rnds[12];
|
||||
buf[offset++] = rnds[13];
|
||||
buf[offset++] = rnds[14];
|
||||
buf[offset++] = rnds[15];
|
||||
return buf;
|
||||
}
|
||||
var v7_default = v7;
|
||||
|
||||
// src/index.ts
|
||||
var plugin = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: "uuid.v1",
|
||||
description: "Generate a UUID V1",
|
||||
args: [],
|
||||
async onRender(_ctx, _args) {
|
||||
return v1_default();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "uuid.v3",
|
||||
description: "Generate a UUID V3",
|
||||
args: [
|
||||
{ type: "text", name: "name", label: "Name" },
|
||||
{
|
||||
type: "text",
|
||||
name: "namespace",
|
||||
label: "Namespace UUID",
|
||||
description: "A valid UUID to use as the namespace",
|
||||
placeholder: "24ced880-3bf4-11f0-8329-cd053d577f0e"
|
||||
}
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
return v3_default(String(args.values.name), String(args.values.namespace));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "uuid.v4",
|
||||
description: "Generate a UUID V4",
|
||||
args: [],
|
||||
async onRender(_ctx, _args) {
|
||||
return v4_default();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "uuid.v5",
|
||||
description: "Generate a UUID V5",
|
||||
args: [
|
||||
{ type: "text", name: "name", label: "Name" },
|
||||
{ type: "text", name: "namespace", label: "Namespace" }
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
return v5_default(String(args.values.name), String(args.values.namespace));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "uuid.v6",
|
||||
description: "Generate a UUID V6",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "timestamp",
|
||||
label: "Timestamp",
|
||||
optional: true,
|
||||
description: "Can be any format that can be parsed by JavaScript new Date(...)",
|
||||
placeholder: "2025-05-28T11:15:00Z"
|
||||
}
|
||||
],
|
||||
async onRender(_ctx, args) {
|
||||
return v6_default({ msecs: new Date(String(args.values.timestamp)).getTime() });
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "uuid.v7",
|
||||
description: "Generate a UUID V7",
|
||||
args: [],
|
||||
async onRender(_ctx, _args) {
|
||||
return v7_default();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
12
src-tauri/vendored/plugins/template-function-uuid/package.json
generated
Normal file
12
src-tauri/vendored/plugins/template-function-uuid/package.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-uuid",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
8380
src-tauri/vendored/plugins/template-function-xml/build/index.js
generated
Normal file
8380
src-tauri/vendored/plugins/template-function-xml/build/index.js
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
src-tauri/vendored/plugins/template-function-xml/package.json
generated
Executable file
13
src-tauri/vendored/plugins/template-function-xml/package.json
generated
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-xml",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "^0.0.34"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ pub(crate) fn decrypt_data(cipher_data: &[u8], key: &Key<XChaCha20Poly1305>) ->
|
||||
let (nonce, ciphered_data) = rest.split_at_checked(nonce_bytes).ok_or(InvalidEncryptedData)?;
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(&key);
|
||||
cipher.decrypt(nonce.into(), ciphered_data).map_err(|_| DecryptionError)
|
||||
cipher.decrypt(nonce.into(), ciphered_data).map_err(|_e| DecryptionError)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -16,13 +16,16 @@ pub enum Error {
|
||||
#[error("Incorrect workspace key")]
|
||||
IncorrectWorkspaceKey,
|
||||
|
||||
#[error("Failed to decrypt workspace key: {0}")]
|
||||
WorkspaceKeyDecryptionError(String),
|
||||
|
||||
#[error("Crypto IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Failed to encrypt")]
|
||||
#[error("Failed to encrypt data")]
|
||||
EncryptionError,
|
||||
|
||||
#[error("Failed to decrypt")]
|
||||
#[error("Failed to decrypt data")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("Invalid encrypted data")]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::error::Error::{GenericError, IncorrectWorkspaceKey, MissingWorkspaceKey};
|
||||
use crate::error::Error::{
|
||||
GenericError, IncorrectWorkspaceKey, MissingWorkspaceKey, WorkspaceKeyDecryptionError,
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::master_key::MasterKey;
|
||||
use crate::workspace_key::WorkspaceKey;
|
||||
@@ -149,8 +151,10 @@ impl EncryptionManager {
|
||||
let mkey = self.get_master_key()?;
|
||||
let decoded_key = BASE64_STANDARD
|
||||
.decode(key.encrypted_key)
|
||||
.map_err(|e| GenericError(format!("Failed to decode workspace key {e:?}")))?;
|
||||
let raw_key = mkey.decrypt(decoded_key.as_slice())?;
|
||||
.map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
|
||||
let raw_key = mkey
|
||||
.decrypt(decoded_key.as_slice())
|
||||
.map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
|
||||
info!("Got existing workspace key for {workspace_id}");
|
||||
let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());
|
||||
|
||||
@@ -158,15 +162,14 @@ impl EncryptionManager {
|
||||
}
|
||||
|
||||
fn get_master_key(&self) -> Result<MasterKey> {
|
||||
{
|
||||
let master_secret = self.cached_master_key.lock().unwrap();
|
||||
if let Some(k) = master_secret.as_ref() {
|
||||
return Ok(k.to_owned());
|
||||
}
|
||||
// NOTE: This locks the key for the entire function which seems wrong, but this prevents
|
||||
// concurrent access from prompting the user for a keychain password multiple times.
|
||||
let mut master_secret = self.cached_master_key.lock().unwrap();
|
||||
if let Some(k) = master_secret.as_ref() {
|
||||
return Ok(k.to_owned());
|
||||
}
|
||||
|
||||
let mkey = MasterKey::get_or_create(&self.app_id, KEY_USER)?;
|
||||
let mut master_secret = self.cached_master_key.lock().unwrap();
|
||||
*master_secret = Some(mkey.clone());
|
||||
Ok(mkey)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -20,4 +18,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::manager::decorate_req;
|
||||
use crate::transport::get_transport;
|
||||
use async_recursion::async_recursion;
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use log::debug;
|
||||
use std::collections::BTreeMap;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::Request;
|
||||
use tonic::body::BoxBody;
|
||||
use tonic::transport::Uri;
|
||||
use tonic::Request;
|
||||
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
|
||||
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
|
||||
use tonic_reflection::pb::v1::{
|
||||
@@ -44,6 +46,7 @@ impl AutoReflectionClient {
|
||||
pub async fn send_reflection_request(
|
||||
&mut self,
|
||||
message: MessageRequest,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<MessageResponse, String> {
|
||||
let reflection_request = ServerReflectionRequest {
|
||||
host: "".into(), // Doesn't matter
|
||||
@@ -51,7 +54,9 @@ impl AutoReflectionClient {
|
||||
};
|
||||
|
||||
if self.use_v1alpha {
|
||||
let request = Request::new(tokio_stream::once(to_v1alpha_request(reflection_request)));
|
||||
let mut request = Request::new(tokio_stream::once(to_v1alpha_request(reflection_request)));
|
||||
decorate_req(metadata, &mut request).map_err(|e| e.to_string())?;
|
||||
|
||||
self.client_v1alpha
|
||||
.server_reflection_info(request)
|
||||
.await
|
||||
@@ -70,7 +75,9 @@ impl AutoReflectionClient {
|
||||
.ok_or("No reflection response".to_string())
|
||||
.map(|resp| to_v1_msg_response(resp))
|
||||
} else {
|
||||
let request = Request::new(tokio_stream::once(reflection_request));
|
||||
let mut request = Request::new(tokio_stream::once(reflection_request));
|
||||
decorate_req(metadata, &mut request).map_err(|e| e.to_string())?;
|
||||
|
||||
let resp = self.client_v1.server_reflection_info(request).await;
|
||||
match resp {
|
||||
Ok(r) => Ok(r),
|
||||
@@ -79,7 +86,7 @@ impl AutoReflectionClient {
|
||||
// If v1 fails, change to v1alpha and try again
|
||||
debug!("gRPC schema reflection falling back to v1alpha");
|
||||
self.use_v1alpha = true;
|
||||
return self.send_reflection_request(message).await;
|
||||
return self.send_reflection_request(message, metadata).await;
|
||||
}
|
||||
_ => Err(e),
|
||||
},
|
||||
|
||||
@@ -1,16 +1,256 @@
|
||||
use prost_reflect::{DescriptorPool, MessageDescriptor};
|
||||
use prost_types::field_descriptor_proto;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use prost_reflect::{DescriptorPool, FieldDescriptor, MessageDescriptor};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub fn message_to_json_schema(_: &DescriptorPool, root_msg: MessageDescriptor) -> JsonSchemaEntry {
|
||||
JsonSchemaGenerator::generate_json_schema(root_msg)
|
||||
}
|
||||
|
||||
struct JsonSchemaGenerator {
|
||||
msg_mapping: HashMap<String, JsonSchemaEntry>,
|
||||
}
|
||||
|
||||
impl JsonSchemaGenerator {
|
||||
pub fn new() -> Self {
|
||||
JsonSchemaGenerator {
|
||||
msg_mapping: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_json_schema(msg: MessageDescriptor) -> JsonSchemaEntry {
|
||||
let generator = JsonSchemaGenerator::new();
|
||||
generator.scan_root(msg)
|
||||
}
|
||||
|
||||
fn add_message(&mut self, msg: &MessageDescriptor) {
|
||||
let name = msg.full_name().to_string();
|
||||
if self.msg_mapping.contains_key(&name) {
|
||||
return;
|
||||
}
|
||||
self.msg_mapping.insert(name.clone(), JsonSchemaEntry::object());
|
||||
}
|
||||
|
||||
pub fn scan_root(mut self, root_msg: MessageDescriptor) -> JsonSchemaEntry {
|
||||
self.init_structure(root_msg.clone());
|
||||
self.fill_properties(root_msg.clone());
|
||||
|
||||
let mut root = self.msg_mapping.remove(root_msg.full_name()).unwrap();
|
||||
|
||||
if self.msg_mapping.len() > 0 {
|
||||
root.defs = Some(self.msg_mapping);
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
fn fill_properties(&mut self, root_msg: MessageDescriptor) {
|
||||
let root_name = root_msg.full_name().to_string();
|
||||
|
||||
let mut visited = HashSet::new();
|
||||
let mut msg_queue = VecDeque::new();
|
||||
msg_queue.push_back(root_msg);
|
||||
|
||||
while !msg_queue.is_empty() {
|
||||
let msg = msg_queue.pop_front().unwrap();
|
||||
let msg_name = msg.full_name();
|
||||
if visited.contains(msg_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.insert(msg_name.to_string());
|
||||
|
||||
let entry = self.msg_mapping.get_mut(msg_name).unwrap();
|
||||
|
||||
for field in msg.fields() {
|
||||
let field_name = field.name().to_string();
|
||||
|
||||
if matches!(field.cardinality(), prost_reflect::Cardinality::Required) {
|
||||
entry.add_required(field_name.clone());
|
||||
}
|
||||
|
||||
if let Some(oneof) = field.containing_oneof() {
|
||||
for oneof_field in oneof.fields() {
|
||||
if let Some(fm) = is_message_field(&oneof_field) {
|
||||
msg_queue.push_back(fm);
|
||||
}
|
||||
entry.add_property(
|
||||
oneof_field.name().to_string(),
|
||||
field_to_type_or_ref(&root_name, oneof_field),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let (field_type, nest_msg) = {
|
||||
if let Some(fm) = is_message_field(&field) {
|
||||
if field.is_list() {
|
||||
// repeated message type
|
||||
(
|
||||
JsonSchemaEntry::array(field_to_type_or_ref(&root_name, field)),
|
||||
Some(fm),
|
||||
)
|
||||
} else if field.is_map() {
|
||||
let value_field = fm.get_field_by_name("value").unwrap();
|
||||
|
||||
if let Some(fm) = is_message_field(&value_field) {
|
||||
(
|
||||
JsonSchemaEntry::map(field_to_type_or_ref(
|
||||
&root_name,
|
||||
value_field,
|
||||
)),
|
||||
Some(fm),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
JsonSchemaEntry::map(field_to_type_or_ref(
|
||||
&root_name,
|
||||
value_field,
|
||||
)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(field_to_type_or_ref(&root_name, field), Some(fm))
|
||||
}
|
||||
} else {
|
||||
if field.is_list() {
|
||||
// repeated scalar type
|
||||
(JsonSchemaEntry::array(field_to_type_or_ref(&root_name, field)), None)
|
||||
} else {
|
||||
(field_to_type_or_ref(&root_name, field), None)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(fm) = nest_msg {
|
||||
msg_queue.push_back(fm);
|
||||
}
|
||||
|
||||
entry.add_property(field_name, field_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_structure(&mut self, root_msg: MessageDescriptor) {
|
||||
let mut visited = HashSet::new();
|
||||
let mut msg_queue = VecDeque::new();
|
||||
msg_queue.push_back(root_msg.clone());
|
||||
|
||||
// level traversal, to make sure all message type is defined before used
|
||||
while !msg_queue.is_empty() {
|
||||
let msg = msg_queue.pop_front().unwrap();
|
||||
let name = msg.full_name();
|
||||
if visited.contains(name) {
|
||||
continue;
|
||||
}
|
||||
visited.insert(name.to_string());
|
||||
self.add_message(&msg);
|
||||
|
||||
for child in msg.child_messages() {
|
||||
if child.is_map_entry() {
|
||||
// for field with map<key, value> type, there will be a child message type *Entry generated
|
||||
// just skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
self.add_message(&child);
|
||||
msg_queue.push_back(child);
|
||||
}
|
||||
|
||||
for field in msg.fields() {
|
||||
if let Some(oneof) = field.containing_oneof() {
|
||||
for oneof_field in oneof.fields() {
|
||||
if let Some(fm) = is_message_field(&oneof_field) {
|
||||
self.add_message(&fm);
|
||||
msg_queue.push_back(fm);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if field.is_map() {
|
||||
// key is always scalar type, so no need to process
|
||||
// value can be any type, so need to unpack value type
|
||||
let map_field_msg = is_message_field(&field).unwrap();
|
||||
let map_value_field = map_field_msg.get_field_by_name("value").unwrap();
|
||||
if let Some(value_fm) = is_message_field(&map_value_field) {
|
||||
self.add_message(&value_fm);
|
||||
msg_queue.push_back(value_fm);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(fm) = is_message_field(&field) {
|
||||
self.add_message(&fm);
|
||||
msg_queue.push_back(fm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field_to_type_or_ref(root_name: &str, field: FieldDescriptor) -> JsonSchemaEntry {
|
||||
match field.kind() {
|
||||
prost_reflect::Kind::Bool => JsonSchemaEntry::boolean(),
|
||||
prost_reflect::Kind::Double => JsonSchemaEntry::number("double"),
|
||||
prost_reflect::Kind::Float => JsonSchemaEntry::number("float"),
|
||||
prost_reflect::Kind::Int32 => JsonSchemaEntry::number("int32"),
|
||||
prost_reflect::Kind::Int64 => JsonSchemaEntry::string_with_format("int64"),
|
||||
prost_reflect::Kind::Uint32 => JsonSchemaEntry::number("int64"),
|
||||
prost_reflect::Kind::Uint64 => JsonSchemaEntry::string_with_format("uint64"),
|
||||
prost_reflect::Kind::Sint32 => JsonSchemaEntry::number("sint32"),
|
||||
prost_reflect::Kind::Sint64 => JsonSchemaEntry::string_with_format("sint64"),
|
||||
prost_reflect::Kind::Fixed32 => JsonSchemaEntry::number("int64"),
|
||||
prost_reflect::Kind::Fixed64 => JsonSchemaEntry::string_with_format("fixed64"),
|
||||
prost_reflect::Kind::Sfixed32 => JsonSchemaEntry::number("sfixed32"),
|
||||
prost_reflect::Kind::Sfixed64 => JsonSchemaEntry::string_with_format("sfixed64"),
|
||||
prost_reflect::Kind::String => JsonSchemaEntry::string(),
|
||||
prost_reflect::Kind::Bytes => JsonSchemaEntry::string_with_format("byte"),
|
||||
prost_reflect::Kind::Enum(enums) => {
|
||||
let values = enums.values().map(|v| v.name().to_string()).collect::<Vec<_>>();
|
||||
JsonSchemaEntry::enums(values)
|
||||
}
|
||||
prost_reflect::Kind::Message(fm) => {
|
||||
let field_type_full_name = fm.full_name();
|
||||
match field_type_full_name {
|
||||
// [Protocol Buffers Well-Known Types]: https://protobuf.dev/reference/protobuf/google.protobuf/
|
||||
"google.protobuf.FieldMask" => JsonSchemaEntry::string(),
|
||||
"google.protobuf.Timestamp" => JsonSchemaEntry::string_with_format("date-time"),
|
||||
"google.protobuf.Duration" => JsonSchemaEntry::string(),
|
||||
"google.protobuf.StringValue" => JsonSchemaEntry::string(),
|
||||
"google.protobuf.BytesValue" => JsonSchemaEntry::string_with_format("byte"),
|
||||
"google.protobuf.Int32Value" => JsonSchemaEntry::number("int32"),
|
||||
"google.protobuf.UInt32Value" => JsonSchemaEntry::string_with_format("int64"),
|
||||
"google.protobuf.Int64Value" => JsonSchemaEntry::string_with_format("int64"),
|
||||
"google.protobuf.UInt64Value" => JsonSchemaEntry::string_with_format("uint64"),
|
||||
"google.protobuf.FloatValue" => JsonSchemaEntry::number("float"),
|
||||
"google.protobuf.DoubleValue" => JsonSchemaEntry::number("double"),
|
||||
"google.protobuf.BoolValue" => JsonSchemaEntry::boolean(),
|
||||
"google.protobuf.Empty" => JsonSchemaEntry::default(),
|
||||
"google.protobuf.Struct" => JsonSchemaEntry::object(),
|
||||
"google.protobuf.ListValue" => JsonSchemaEntry::array(JsonSchemaEntry::default()),
|
||||
"google.protobuf.NullValue" => JsonSchemaEntry::null(),
|
||||
name @ _ if name == root_name => JsonSchemaEntry::root_reference(),
|
||||
_ => JsonSchemaEntry::reference(fm.full_name()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_message_field(field: &FieldDescriptor) -> Option<MessageDescriptor> {
|
||||
match field.kind() {
|
||||
prost_reflect::Kind::Message(m) => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct JsonSchemaEntry {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
title: Option<String>,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
type_: JsonType,
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
type_: Option<JsonType>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
@@ -21,15 +261,115 @@ pub struct JsonSchemaEntry {
|
||||
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
|
||||
enum_: Option<Vec<String>>,
|
||||
|
||||
/// Don't allow any other properties in the object
|
||||
additional_properties: bool,
|
||||
// for map type
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
additional_properties: Option<Box<JsonSchemaEntry>>,
|
||||
|
||||
/// Set all properties to required
|
||||
// Set all properties to required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
required: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
items: Option<Box<JsonSchemaEntry>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "$defs")]
|
||||
defs: Option<HashMap<String, JsonSchemaEntry>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "$ref")]
|
||||
ref_: Option<String>,
|
||||
}
|
||||
|
||||
impl JsonSchemaEntry {
|
||||
pub fn add_property(&mut self, name: String, entry: JsonSchemaEntry) {
|
||||
if self.properties.is_none() {
|
||||
self.properties = Some(HashMap::new());
|
||||
}
|
||||
self.properties.as_mut().unwrap().insert(name, entry);
|
||||
}
|
||||
|
||||
pub fn add_required(&mut self, name: String) {
|
||||
if self.required.is_none() {
|
||||
self.required = Some(Vec::new());
|
||||
}
|
||||
self.required.as_mut().unwrap().push(name);
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchemaEntry {
|
||||
pub fn object() -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Object),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn boolean() -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Boolean),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn number<S: Into<String>>(format: S) -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Number),
|
||||
format: Some(format.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn string() -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::String),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_with_format<S: Into<String>>(format: S) -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::String),
|
||||
format: Some(format.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn reference<S: AsRef<str>>(ref_: S) -> Self {
|
||||
JsonSchemaEntry {
|
||||
ref_: Some(format!("#/$defs/{}", ref_.as_ref())),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn root_reference() -> Self{
|
||||
JsonSchemaEntry {
|
||||
ref_: Some("#".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn array(item: JsonSchemaEntry) -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Array),
|
||||
items: Some(Box::new(item)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn enums(enums: Vec<String>) -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::String),
|
||||
enum_: Some(enums),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map(value_type: JsonSchemaEntry) -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Object),
|
||||
additional_properties: Some(Box::new(value_type)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn null() -> Self {
|
||||
JsonSchemaEntry {
|
||||
type_: Some(JsonType::Null),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum JsonType {
|
||||
@@ -49,7 +389,7 @@ impl Default for JsonType {
|
||||
}
|
||||
|
||||
impl serde::Serialize for JsonType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
@@ -64,116 +404,3 @@ impl serde::Serialize for JsonType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for JsonType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"string" => Ok(JsonType::String),
|
||||
"number" => Ok(JsonType::Number),
|
||||
"object" => Ok(JsonType::Object),
|
||||
"array" => Ok(JsonType::Array),
|
||||
"boolean" => Ok(JsonType::Boolean),
|
||||
"null" => Ok(JsonType::Null),
|
||||
_ => Ok(JsonType::_UNKNOWN),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_to_json_schema(
|
||||
pool: &DescriptorPool,
|
||||
message: MessageDescriptor,
|
||||
) -> JsonSchemaEntry {
|
||||
let mut schema = JsonSchemaEntry {
|
||||
title: Some(message.name().to_string()),
|
||||
type_: JsonType::Object, // Messages are objects
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
message.fields().for_each(|f| match f.kind() {
|
||||
prost_reflect::Kind::Message(m) => {
|
||||
properties.insert(f.name().to_string(), message_to_json_schema(pool, m));
|
||||
}
|
||||
prost_reflect::Kind::Enum(e) => {
|
||||
properties.insert(
|
||||
f.name().to_string(),
|
||||
JsonSchemaEntry {
|
||||
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
|
||||
enum_: Some(e.values().map(|v| v.name().to_string()).collect::<Vec<_>>()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
// TODO: Handle repeated label
|
||||
match f.field_descriptor_proto().label() {
|
||||
field_descriptor_proto::Label::Repeated => {
|
||||
// TODO: Handle more complex repeated types. This just handles primitives for now
|
||||
properties.insert(
|
||||
f.name().to_string(),
|
||||
JsonSchemaEntry {
|
||||
type_: JsonType::Array,
|
||||
items: Some(Box::new(JsonSchemaEntry {
|
||||
type_: map_proto_type_to_json_type(
|
||||
f.field_descriptor_proto().r#type(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
// Regular JSON field
|
||||
properties.insert(
|
||||
f.name().to_string(),
|
||||
JsonSchemaEntry {
|
||||
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
schema.properties = Some(properties);
|
||||
|
||||
// All proto 3 fields are optional, so maybe we could
|
||||
// make this a setting?
|
||||
// schema.required = Some(
|
||||
// message
|
||||
// .fields()
|
||||
// .map(|f| f.name().to_string())
|
||||
// .collect::<Vec<_>>(),
|
||||
// );
|
||||
|
||||
schema
|
||||
}
|
||||
|
||||
fn map_proto_type_to_json_type(proto_type: field_descriptor_proto::Type) -> JsonType {
|
||||
match proto_type {
|
||||
field_descriptor_proto::Type::Double => JsonType::Number,
|
||||
field_descriptor_proto::Type::Float => JsonType::Number,
|
||||
field_descriptor_proto::Type::Int64 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Uint64 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Int32 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Fixed64 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Fixed32 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Bool => JsonType::Boolean,
|
||||
field_descriptor_proto::Type::String => JsonType::String,
|
||||
field_descriptor_proto::Type::Group => JsonType::_UNKNOWN,
|
||||
field_descriptor_proto::Type::Message => JsonType::Object,
|
||||
field_descriptor_proto::Type::Bytes => JsonType::String,
|
||||
field_descriptor_proto::Type::Uint32 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Enum => JsonType::String,
|
||||
field_descriptor_proto::Type::Sfixed32 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Sfixed64 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Sint32 => JsonType::Number,
|
||||
field_descriptor_proto::Type::Sint64 => JsonType::Number,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
message: &str,
|
||||
metadata: BTreeMap<String, String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<DynamicMessage>, StreamError> {
|
||||
let method = &self.method(&service, &method)?;
|
||||
let input_message = method.input();
|
||||
@@ -96,7 +96,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<DynamicMessage>,
|
||||
metadata: BTreeMap<String, String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>, StreamError> {
|
||||
let method = &self.method(&service, &method)?;
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
@@ -116,7 +116,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<DynamicMessage>,
|
||||
metadata: BTreeMap<String, String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<DynamicMessage>, StreamError> {
|
||||
let method = &self.method(&service, &method)?;
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
@@ -137,7 +137,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
message: &str,
|
||||
metadata: BTreeMap<String, String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>, StreamError> {
|
||||
let method = &self.method(&service, &method)?;
|
||||
let input_message = method.input();
|
||||
@@ -180,10 +180,11 @@ impl GrpcHandle {
|
||||
id: &str,
|
||||
uri: &str,
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
let pool = if proto_files.is_empty() {
|
||||
let full_uri = uri_from_str(uri)?;
|
||||
fill_pool_from_reflection(&full_uri).await
|
||||
fill_pool_from_reflection(&full_uri, metadata).await
|
||||
} else {
|
||||
fill_pool_from_files(&self.app_handle, proto_files).await
|
||||
}?;
|
||||
@@ -197,9 +198,10 @@ impl GrpcHandle {
|
||||
id: &str,
|
||||
uri: &str,
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
// Ensure reflection is up-to-date
|
||||
self.reflect(id, uri, proto_files).await?;
|
||||
self.reflect(id, uri, proto_files, metadata).await?;
|
||||
|
||||
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool".to_string())?;
|
||||
Ok(self.services_from_pool(&pool))
|
||||
@@ -235,8 +237,9 @@ impl GrpcHandle {
|
||||
id: &str,
|
||||
uri: &str,
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<GrpcConnection, String> {
|
||||
self.reflect(id, uri, proto_files).await?;
|
||||
self.reflect(id, uri, proto_files, metadata).await?;
|
||||
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool")?;
|
||||
|
||||
let uri = uri_from_str(uri)?;
|
||||
@@ -254,7 +257,10 @@ impl GrpcHandle {
|
||||
}
|
||||
}
|
||||
|
||||
fn decorate_req<T>(metadata: BTreeMap<String, String>, req: &mut Request<T>) -> Result<(), String> {
|
||||
pub(crate) fn decorate_req<T>(
|
||||
metadata: &BTreeMap<String, String>,
|
||||
req: &mut Request<T>,
|
||||
) -> Result<(), String> {
|
||||
for (k, v) in metadata {
|
||||
req.metadata_mut().insert(
|
||||
MetadataKey::from_str(k.as_str()).map_err(|e| e.to_string())?,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::env::temp_dir;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
@@ -89,11 +90,14 @@ pub async fn fill_pool_from_files(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, String> {
|
||||
pub async fn fill_pool_from_reflection(
|
||||
uri: &Uri,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<DescriptorPool, String> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let mut client = AutoReflectionClient::new(uri);
|
||||
|
||||
for service in list_services(&mut client).await? {
|
||||
for service in list_services(&mut client, metadata).await? {
|
||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||
continue;
|
||||
}
|
||||
@@ -101,14 +105,18 @@ pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, Stri
|
||||
// TODO: update reflection client to use v1
|
||||
continue;
|
||||
}
|
||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client, metadata).await;
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn list_services(client: &mut AutoReflectionClient) -> Result<Vec<String>, String> {
|
||||
let response = client.send_reflection_request(MessageRequest::ListServices("".into())).await?;
|
||||
async fn list_services(
|
||||
client: &mut AutoReflectionClient,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let response =
|
||||
client.send_reflection_request(MessageRequest::ListServices("".into()), metadata).await?;
|
||||
|
||||
let list_services_response = match response {
|
||||
MessageResponse::ListServicesResponse(resp) => resp,
|
||||
@@ -122,9 +130,13 @@ async fn file_descriptor_set_from_service_name(
|
||||
service_name: &str,
|
||||
pool: &mut DescriptorPool,
|
||||
client: &mut AutoReflectionClient,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) {
|
||||
let response = match client
|
||||
.send_reflection_request(MessageRequest::FileContainingSymbol(service_name.into()))
|
||||
.send_reflection_request(
|
||||
MessageRequest::FileContainingSymbol(service_name.into()),
|
||||
metadata,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
@@ -139,8 +151,13 @@ async fn file_descriptor_set_from_service_name(
|
||||
_ => panic!("Expected a FileDescriptorResponse variant"),
|
||||
};
|
||||
|
||||
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
|
||||
.await;
|
||||
add_file_descriptors_to_pool(
|
||||
file_descriptor_response.file_descriptor_proto,
|
||||
pool,
|
||||
client,
|
||||
metadata,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -148,6 +165,7 @@ async fn add_file_descriptors_to_pool(
|
||||
fds: Vec<Vec<u8>>,
|
||||
pool: &mut DescriptorPool,
|
||||
client: &mut AutoReflectionClient,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) {
|
||||
let mut topo_sort = topology::SimpleTopoSort::new();
|
||||
let mut fd_mapping = std::collections::HashMap::with_capacity(fds.len());
|
||||
@@ -165,7 +183,7 @@ async fn add_file_descriptors_to_pool(
|
||||
if let Some(fdp) = fd_mapping.remove(&node) {
|
||||
pool.add_file_descriptor_proto(fdp).expect("add file descriptor proto");
|
||||
} else {
|
||||
file_descriptor_set_by_filename(node.as_str(), pool, client).await;
|
||||
file_descriptor_set_by_filename(node.as_str(), pool, client, metadata).await;
|
||||
}
|
||||
}
|
||||
Err(_) => panic!("proto file got cycle!"),
|
||||
@@ -177,6 +195,7 @@ async fn file_descriptor_set_by_filename(
|
||||
filename: &str,
|
||||
pool: &mut DescriptorPool,
|
||||
client: &mut AutoReflectionClient,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) {
|
||||
// We already fetched this file
|
||||
if let Some(_) = pool.get_file_by_name(filename) {
|
||||
@@ -184,7 +203,7 @@ async fn file_descriptor_set_by_filename(
|
||||
}
|
||||
|
||||
let msg = MessageRequest::FileByFilename(filename.into());
|
||||
let response = client.send_reflection_request(msg).await;
|
||||
let response = client.send_reflection_request(msg, metadata).await;
|
||||
let file_descriptor_response = match response {
|
||||
Ok(MessageResponse::FileDescriptorResponse(resp)) => resp,
|
||||
Ok(_) => {
|
||||
@@ -196,8 +215,13 @@ async fn file_descriptor_set_by_filename(
|
||||
}
|
||||
};
|
||||
|
||||
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
|
||||
.await;
|
||||
add_file_descriptors_to_pool(
|
||||
file_descriptor_response.file_descriptor_proto,
|
||||
pool,
|
||||
client,
|
||||
metadata,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
use crate::error::Result;
|
||||
use crate::{
|
||||
activate_license, check_license, deactivate_license, ActivateLicenseRequestPayload,
|
||||
CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus,
|
||||
};
|
||||
use crate::{LicenseCheckStatus, activate_license, check_license, deactivate_license};
|
||||
use log::{debug, info};
|
||||
use std::string::ToString;
|
||||
use tauri::{command, Manager, Runtime, WebviewWindow};
|
||||
use tauri::{Runtime, WebviewWindow, command};
|
||||
|
||||
#[command]
|
||||
pub async fn check<R: Runtime>(window: WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||
debug!("Checking license");
|
||||
check_license(
|
||||
&window,
|
||||
CheckActivationRequestPayload {
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
check_license(&window).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -> Result<()> {
|
||||
info!("Activating license {}", license_key);
|
||||
activate_license(
|
||||
&window,
|
||||
ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
activate_license(&window, license_key).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn deactivate<R: Runtime>(window: WebviewWindow<R>) -> Result<()> {
|
||||
info!("Deactivating activation");
|
||||
deactivate_license(
|
||||
&window,
|
||||
DeactivateLicenseRequestPayload {
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
deactivate_license(&window).await
|
||||
}
|
||||
|
||||
@@ -16,3 +16,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.invoke_handler(generate_handler![check, activate, deactivate])
|
||||
.build()
|
||||
}
|
||||
|
||||
pub(crate) fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
use std::time::Duration;
|
||||
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||
use ts_rs::TS;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -63,10 +63,15 @@ pub struct APIErrorResponsePayload {
|
||||
|
||||
pub async fn activate_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
p: ActivateLicenseRequestPayload,
|
||||
license_key: &str,
|
||||
) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(build_url("/licenses/activate")).json(&p).send().await?;
|
||||
let payload = ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
};
|
||||
let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
@@ -95,16 +100,17 @@ pub async fn activate_license<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn deactivate_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
p: DeactivateLicenseRequestPayload,
|
||||
) -> Result<()> {
|
||||
pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<()> {
|
||||
let app_handle = window.app_handle();
|
||||
let activation_id = get_activation_id(app_handle).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let path = format!("/licenses/activations/{}/deactivate", activation_id);
|
||||
let response = client.post(build_url(&path)).json(&p).send().await?;
|
||||
let payload = DeactivateLicenseRequestPayload {
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
};
|
||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
@@ -141,10 +147,11 @@ pub enum LicenseCheckStatus {
|
||||
Trialing { end: NaiveDateTime },
|
||||
}
|
||||
|
||||
pub async fn check_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
payload: CheckActivationRequestPayload,
|
||||
) -> Result<LicenseCheckStatus> {
|
||||
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||
let payload = CheckActivationRequestPayload {
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.package_info().version.to_string(),
|
||||
};
|
||||
let activation_id = get_activation_id(window.app_handle()).await;
|
||||
let settings = window.db().get_settings();
|
||||
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
|
||||
@@ -197,9 +204,5 @@ fn build_url(path: &str) -> String {
|
||||
}
|
||||
|
||||
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
|
||||
app_handle.db().get_key_value_string(
|
||||
KV_ACTIVATION_ID_KEY,
|
||||
KV_NAMESPACE,
|
||||
"",
|
||||
)
|
||||
app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||
|
||||
@@ -28,9 +28,7 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
|
||||
|
||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -50,15 +48,19 @@ export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
|
||||
|
||||
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
|
||||
|
||||
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
|
||||
|
||||
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
||||
|
||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
||||
|
||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
|
||||
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
@@ -76,6 +78,6 @@ export type WebsocketMessageType = "text" | "binary";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
|
||||
@@ -53,9 +53,10 @@ let _activeWorkspaceId: string | null = null;
|
||||
|
||||
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
||||
console.log('Syncing models with new workspace', workspaceId);
|
||||
const workspaceModels = await invoke<AnyModel[]>('plugin:yaak-models|workspace_models', {
|
||||
const workspaceModelsStr = await invoke<string>('plugin:yaak-models|workspace_models', {
|
||||
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
|
||||
});
|
||||
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
|
||||
const data = newStoreData();
|
||||
for (const model of workspaceModels) {
|
||||
data[model.model][model.id] = model;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createStore } from 'jotai/index';
|
||||
import { createStore } from 'jotai';
|
||||
import { AnyModel } from '../bindings/gen_models';
|
||||
|
||||
export type ExtractModel<T, M> = T extends { model: M } ? T : never;
|
||||
|
||||
@@ -94,7 +94,7 @@ pub(crate) fn get_settings<R: Runtime>(app_handle: AppHandle<R>) -> Result<Setti
|
||||
pub(crate) fn workspace_models<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: Option<&str>,
|
||||
) -> Result<Vec<AnyModel>> {
|
||||
) -> Result<String> {
|
||||
let db = window.db();
|
||||
let mut l: Vec<AnyModel> = Vec::new();
|
||||
|
||||
@@ -109,7 +109,7 @@ pub(crate) fn workspace_models<R: Runtime>(
|
||||
// Add the workspace children
|
||||
if let Some(wid) = workspace_id {
|
||||
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
|
||||
l.append(&mut db.list_environments(wid)?.into_iter().map(Into::into).collect());
|
||||
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
|
||||
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
|
||||
l.append(&mut db.list_grpc_connections(wid)?.into_iter().map(Into::into).collect());
|
||||
l.append(&mut db.list_grpc_requests(wid)?.into_iter().map(Into::into).collect());
|
||||
@@ -120,5 +120,36 @@ pub(crate) fn workspace_models<R: Runtime>(
|
||||
l.append(&mut db.list_workspace_metas(wid)?.into_iter().map(Into::into).collect());
|
||||
}
|
||||
|
||||
Ok(l)
|
||||
let j = serde_json::to_string(&l)?;
|
||||
|
||||
// NOTE: There's something weird that happens on Linux. If we send Cyrillic (or maybe other)
|
||||
// unicode characters in this response (doesn't matter where) then the following bug happens:
|
||||
// https://feedback.yaak.app/p/editing-the-url-sometimes-freezes-the-app
|
||||
//
|
||||
// It's as if every string resulting from the JSON.parse of the models gets encoded slightly
|
||||
// wrong or something, causing the above bug where Codemirror can't calculate the cursor
|
||||
// position anymore (even when none of the characters are included directly in the input).
|
||||
//
|
||||
// For some reason using escape sequences works, but it's a hacky fix. Hopefully the Linux
|
||||
// webview dependency updates to a version where this bug doesn't exist, or we can use CEF
|
||||
// (Chromium) for Linux in the future, which Tauri is working on.
|
||||
Ok(escape_str_for_webview(&j))
|
||||
}
|
||||
|
||||
fn escape_str_for_webview(input: &str) -> String {
|
||||
input.chars().map(|c| {
|
||||
let code = c as u32;
|
||||
// ASCII
|
||||
if code <= 0x7F {
|
||||
c.to_string()
|
||||
// BMP characters encoded normally
|
||||
} else if code < 0xFFFF {
|
||||
format!("\\u{:04X}", code)
|
||||
// Beyond BMP encoded a surrogate pairs
|
||||
} else {
|
||||
let high = ((code - 0x10000) >> 10) + 0xD800;
|
||||
let low = ((code - 0x10000) & 0x3FF) + 0xDC00;
|
||||
format!("\\u{:04X}\\u{:04X}", high, low)
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
@@ -21,6 +21,12 @@ pub enum Error {
|
||||
#[error("Model error: {0}")]
|
||||
GenericError(String),
|
||||
|
||||
#[error("No base environment for {0}")]
|
||||
MissingBaseEnvironment(String),
|
||||
|
||||
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
|
||||
MultipleBaseEnvironments(String),
|
||||
|
||||
#[error("Row not found")]
|
||||
RowNotFound,
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ macro_rules! impl_model {
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub enum ProxySetting {
|
||||
Enabled {
|
||||
#[serde(default)]
|
||||
// This was added after on so give it a default to be able to deserialize older values
|
||||
disabled: bool,
|
||||
http: String,
|
||||
https: String,
|
||||
auth: Option<ProxySettingAuth>,
|
||||
@@ -102,6 +105,7 @@ pub struct Settings {
|
||||
pub appearance: String,
|
||||
pub editor_font_size: i32,
|
||||
pub editor_soft_wrap: bool,
|
||||
pub hide_window_controls: bool,
|
||||
pub interface_font_size: i32,
|
||||
pub interface_scale: f32,
|
||||
pub open_workspace_new_window: Option<bool>,
|
||||
@@ -151,6 +155,7 @@ impl UpsertModelInfo for Settings {
|
||||
(EditorSoftWrap, self.editor_soft_wrap.into()),
|
||||
(InterfaceFontSize, self.interface_font_size.into()),
|
||||
(InterfaceScale, self.interface_scale.into()),
|
||||
(HideWindowControls, self.hide_window_controls.into()),
|
||||
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
|
||||
(ThemeDark, self.theme_dark.as_str().into()),
|
||||
(ThemeLight, self.theme_light.as_str().into()),
|
||||
@@ -168,6 +173,7 @@ impl UpsertModelInfo for Settings {
|
||||
SettingsIden::EditorSoftWrap,
|
||||
SettingsIden::InterfaceFontSize,
|
||||
SettingsIden::InterfaceScale,
|
||||
SettingsIden::HideWindowControls,
|
||||
SettingsIden::OpenWorkspaceNewWindow,
|
||||
SettingsIden::Proxy,
|
||||
SettingsIden::ThemeDark,
|
||||
@@ -197,6 +203,7 @@ impl UpsertModelInfo for Settings {
|
||||
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
|
||||
theme_dark: row.get("theme_dark")?,
|
||||
theme_light: row.get("theme_light")?,
|
||||
hide_window_controls: row.get("hide_window_controls")?,
|
||||
update_channel: row.get("update_channel")?,
|
||||
})
|
||||
}
|
||||
@@ -212,8 +219,13 @@ pub struct Workspace {
|
||||
pub id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub name: String,
|
||||
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub authentication: BTreeMap<String, Value>,
|
||||
pub authentication_type: Option<String>,
|
||||
pub description: String,
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
pub name: String,
|
||||
pub encryption_key_challenge: Option<String>,
|
||||
|
||||
// Settings
|
||||
@@ -254,6 +266,9 @@ impl UpsertModelInfo for Workspace {
|
||||
(CreatedAt, upsert_date(source, self.created_at)),
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(Name, self.name.trim().into()),
|
||||
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
||||
(AuthenticationType, self.authentication_type.into()),
|
||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||
(Description, self.description.into()),
|
||||
(EncryptionKeyChallenge, self.encryption_key_challenge.into()),
|
||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
||||
@@ -266,6 +281,9 @@ impl UpsertModelInfo for Workspace {
|
||||
vec![
|
||||
WorkspaceIden::UpdatedAt,
|
||||
WorkspaceIden::Name,
|
||||
WorkspaceIden::Authentication,
|
||||
WorkspaceIden::AuthenticationType,
|
||||
WorkspaceIden::Headers,
|
||||
WorkspaceIden::Description,
|
||||
WorkspaceIden::EncryptionKeyChallenge,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
@@ -279,6 +297,8 @@ impl UpsertModelInfo for Workspace {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let headers: String = row.get("headers")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -287,6 +307,9 @@ impl UpsertModelInfo for Workspace {
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
encryption_key_challenge: row.get("encryption_key_challenge")?,
|
||||
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
||||
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
||||
authentication_type: row.get("authentication_type")?,
|
||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||
@@ -378,7 +401,7 @@ impl UpsertModelInfo for WorkspaceMeta {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
enum CookieDomain {
|
||||
pub enum CookieDomain {
|
||||
HostOnly(String),
|
||||
Suffix(String),
|
||||
NotPresent,
|
||||
@@ -387,7 +410,7 @@ enum CookieDomain {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
enum CookieExpires {
|
||||
pub enum CookieExpires {
|
||||
AtUtc(String),
|
||||
SessionEnd,
|
||||
}
|
||||
@@ -395,10 +418,10 @@ enum CookieExpires {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct Cookie {
|
||||
raw_cookie: String,
|
||||
domain: CookieDomain,
|
||||
expires: CookieExpires,
|
||||
path: (String, bool),
|
||||
pub raw_cookie: String,
|
||||
pub domain: CookieDomain,
|
||||
pub expires: CookieExpires,
|
||||
pub path: (String, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
@@ -486,11 +509,12 @@ pub struct Environment {
|
||||
pub model: String,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
pub name: String,
|
||||
pub public: bool,
|
||||
pub base: bool,
|
||||
pub variables: Vec<EnvironmentVariable>,
|
||||
}
|
||||
|
||||
@@ -523,9 +547,10 @@ impl UpsertModelInfo for Environment {
|
||||
Ok(vec![
|
||||
(CreatedAt, upsert_date(source, self.created_at)),
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(EnvironmentId, self.environment_id.into()),
|
||||
(WorkspaceId, self.workspace_id.into()),
|
||||
(Base, self.base.into()),
|
||||
(Name, self.name.trim().into()),
|
||||
(Public, self.public.into()),
|
||||
(Variables, serde_json::to_string(&self.variables)?.into()),
|
||||
])
|
||||
}
|
||||
@@ -533,7 +558,9 @@ impl UpsertModelInfo for Environment {
|
||||
fn update_columns() -> Vec<impl IntoIden> {
|
||||
vec![
|
||||
EnvironmentIden::UpdatedAt,
|
||||
EnvironmentIden::Base,
|
||||
EnvironmentIden::Name,
|
||||
EnvironmentIden::Public,
|
||||
EnvironmentIden::Variables,
|
||||
]
|
||||
}
|
||||
@@ -547,10 +574,11 @@ impl UpsertModelInfo for Environment {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
workspace_id: row.get("workspace_id")?,
|
||||
environment_id: row.get("environment_id")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
base: row.get("base")?,
|
||||
name: row.get("name")?,
|
||||
public: row.get("public")?,
|
||||
variables: serde_json::from_str(variables.as_str()).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -569,6 +597,22 @@ pub struct EnvironmentVariable {
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ParentAuthentication {
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub authentication: BTreeMap<String, Value>,
|
||||
pub authentication_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ParentHeaders {
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
@@ -582,8 +626,12 @@ pub struct Folder {
|
||||
pub workspace_id: String,
|
||||
pub folder_id: Option<String>,
|
||||
|
||||
pub name: String,
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub authentication: BTreeMap<String, Value>,
|
||||
pub authentication_type: Option<String>,
|
||||
pub description: String,
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
pub name: String,
|
||||
pub sort_priority: f32,
|
||||
}
|
||||
|
||||
@@ -618,8 +666,11 @@ impl UpsertModelInfo for Folder {
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(WorkspaceId, self.workspace_id.into()),
|
||||
(FolderId, self.folder_id.into()),
|
||||
(Name, self.name.trim().into()),
|
||||
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
||||
(AuthenticationType, self.authentication_type.into()),
|
||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||
(Description, self.description.into()),
|
||||
(Name, self.name.trim().into()),
|
||||
(SortPriority, self.sort_priority.into()),
|
||||
])
|
||||
}
|
||||
@@ -628,6 +679,9 @@ impl UpsertModelInfo for Folder {
|
||||
vec![
|
||||
FolderIden::UpdatedAt,
|
||||
FolderIden::Name,
|
||||
FolderIden::Authentication,
|
||||
FolderIden::AuthenticationType,
|
||||
FolderIden::Headers,
|
||||
FolderIden::Description,
|
||||
FolderIden::FolderId,
|
||||
FolderIden::SortPriority,
|
||||
@@ -638,6 +692,8 @@ impl UpsertModelInfo for Folder {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let headers: String = row.get("headers")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -648,6 +704,9 @@ impl UpsertModelInfo for Folder {
|
||||
folder_id: row.get("folder_id")?,
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
||||
authentication_type: row.get("authentication_type")?,
|
||||
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -770,28 +829,28 @@ impl UpsertModelInfo for HttpRequest {
|
||||
]
|
||||
}
|
||||
|
||||
fn from_row(r: &Row) -> rusqlite::Result<Self> {
|
||||
let url_parameters: String = r.get("url_parameters")?;
|
||||
let body: String = r.get("body")?;
|
||||
let authentication: String = r.get("authentication")?;
|
||||
let headers: String = r.get("headers")?;
|
||||
fn from_row(row: &Row) -> rusqlite::Result<Self> {
|
||||
let url_parameters: String = row.get("url_parameters")?;
|
||||
let body: String = row.get("body")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let headers: String = row.get("headers")?;
|
||||
Ok(Self {
|
||||
id: r.get("id")?,
|
||||
model: r.get("model")?,
|
||||
workspace_id: r.get("workspace_id")?,
|
||||
created_at: r.get("created_at")?,
|
||||
updated_at: r.get("updated_at")?,
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
workspace_id: row.get("workspace_id")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
|
||||
authentication_type: r.get("authentication_type")?,
|
||||
authentication_type: row.get("authentication_type")?,
|
||||
body: serde_json::from_str(body.as_str()).unwrap_or_default(),
|
||||
body_type: r.get("body_type")?,
|
||||
description: r.get("description")?,
|
||||
folder_id: r.get("folder_id")?,
|
||||
body_type: row.get("body_type")?,
|
||||
description: row.get("description")?,
|
||||
folder_id: row.get("folder_id")?,
|
||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||
method: r.get("method")?,
|
||||
name: r.get("name")?,
|
||||
sort_priority: r.get("sort_priority")?,
|
||||
url: r.get("url")?,
|
||||
method: row.get("method")?,
|
||||
name: row.get("name")?,
|
||||
sort_priority: row.get("sort_priority")?,
|
||||
url: row.get("url")?,
|
||||
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -980,7 +1039,7 @@ impl UpsertModelInfo for WebsocketRequest {
|
||||
(WorkspaceId, self.workspace_id.into()),
|
||||
(FolderId, self.folder_id.as_ref().map(|s| s.as_str()).into()),
|
||||
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
||||
(AuthenticationType, self.authentication_type.as_ref().map(|s| s.as_str()).into()),
|
||||
(AuthenticationType, self.authentication_type.into()),
|
||||
(Description, self.description.into()),
|
||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||
(Message, self.message.into()),
|
||||
@@ -1283,19 +1342,6 @@ impl UpsertModelInfo for HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct GrpcMetadataEntry {
|
||||
#[serde(default = "default_true")]
|
||||
#[ts(optional, as = "Option<bool>")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
#[ts(optional, as = "Option<String>")]
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
@@ -1314,7 +1360,7 @@ pub struct GrpcRequest {
|
||||
pub authentication: BTreeMap<String, Value>,
|
||||
pub description: String,
|
||||
pub message: String,
|
||||
pub metadata: Vec<GrpcMetadataEntry>,
|
||||
pub metadata: Vec<HttpRequestHeader>,
|
||||
pub method: Option<String>,
|
||||
pub name: String,
|
||||
pub service: Option<String>,
|
||||
@@ -1989,6 +2035,7 @@ impl<'de> Deserialize<'de> for AnyModel {
|
||||
Some(m) if m == "grpc_event" => AnyModel::GrpcEvent(fv(value).unwrap()),
|
||||
Some(m) if m == "grpc_request" => AnyModel::GrpcRequest(fv(value).unwrap()),
|
||||
Some(m) if m == "http_request" => AnyModel::HttpRequest(fv(value).unwrap()),
|
||||
Some(m) if m == "http_response" => AnyModel::HttpResponse(fv(value).unwrap()),
|
||||
Some(m) if m == "key_value" => AnyModel::KeyValue(fv(value).unwrap()),
|
||||
Some(m) if m == "plugin" => AnyModel::Plugin(fv(value).unwrap()),
|
||||
Some(m) if m == "settings" => AnyModel::Settings(fv(value).unwrap()),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace};
|
||||
use crate::util::{BatchUpsertResult, UpdateSource};
|
||||
use log::info;
|
||||
use crate::db_context::DbContext;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn batch_upsert(
|
||||
@@ -18,64 +18,19 @@ impl<'a> DbContext<'a> {
|
||||
let mut imported_resources = BatchUpsertResult::default();
|
||||
|
||||
if workspaces.len() > 0 {
|
||||
info!("Batch inserting {} workspaces", workspaces.len());
|
||||
for v in workspaces {
|
||||
let x = self.upsert_workspace(&v, source)?;
|
||||
imported_resources.workspaces.push(x.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if environments.len() > 0 {
|
||||
while imported_resources.environments.len() < environments.len() {
|
||||
for v in environments.clone() {
|
||||
if let Some(id) = v.environment_id.clone() {
|
||||
let has_parent_to_import =
|
||||
environments.iter().find(|m| m.id == id).is_some();
|
||||
let imported_parent =
|
||||
imported_resources.environments.iter().find(|m| m.id == id);
|
||||
// If there's also a parent to upsert, wait for that one
|
||||
if imported_parent.is_none() && has_parent_to_import {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(_) = imported_resources.environments.iter().find(|f| f.id == v.id) {
|
||||
continue;
|
||||
}
|
||||
let x = self.upsert_environment(&v, source)?;
|
||||
imported_resources.environments.push(x.clone());
|
||||
}
|
||||
}
|
||||
info!("Imported {} environments", imported_resources.environments.len());
|
||||
}
|
||||
|
||||
if folders.len() > 0 {
|
||||
while imported_resources.folders.len() < folders.len() {
|
||||
for v in folders.clone() {
|
||||
if let Some(id) = v.folder_id.clone() {
|
||||
let has_parent_to_import = folders.iter().find(|m| m.id == id).is_some();
|
||||
let imported_parent =
|
||||
imported_resources.folders.iter().find(|m| m.id == id);
|
||||
// If there's also a parent to upsert, wait for that one
|
||||
if imported_parent.is_none() && has_parent_to_import {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) {
|
||||
continue;
|
||||
}
|
||||
let x = self.upsert_folder(&v, source)?;
|
||||
imported_resources.folders.push(x.clone());
|
||||
}
|
||||
}
|
||||
info!("Imported {} folders", imported_resources.folders.len());
|
||||
info!("Upserted {} workspaces", imported_resources.environments.len());
|
||||
}
|
||||
|
||||
if http_requests.len() > 0 {
|
||||
for v in http_requests {
|
||||
let x = self.upsert(&v, source)?;
|
||||
let x = self.upsert_http_request(&v, source)?;
|
||||
imported_resources.http_requests.push(x.clone());
|
||||
}
|
||||
info!("Imported {} http_requests", imported_resources.http_requests.len());
|
||||
info!("Upserted Imported {} http_requests", imported_resources.http_requests.len());
|
||||
}
|
||||
|
||||
if grpc_requests.len() > 0 {
|
||||
@@ -83,7 +38,7 @@ impl<'a> DbContext<'a> {
|
||||
let x = self.upsert_grpc_request(&v, source)?;
|
||||
imported_resources.grpc_requests.push(x.clone());
|
||||
}
|
||||
info!("Imported {} grpc_requests", imported_resources.grpc_requests.len());
|
||||
info!("Upserted {} grpc_requests", imported_resources.grpc_requests.len());
|
||||
}
|
||||
|
||||
if websocket_requests.len() > 0 {
|
||||
@@ -91,7 +46,25 @@ impl<'a> DbContext<'a> {
|
||||
let x = self.upsert_websocket_request(&v, source)?;
|
||||
imported_resources.websocket_requests.push(x.clone());
|
||||
}
|
||||
info!("Imported {} websocket_requests", imported_resources.websocket_requests.len());
|
||||
info!("Upserted {} websocket_requests", imported_resources.websocket_requests.len());
|
||||
}
|
||||
|
||||
if environments.len() > 0 {
|
||||
for x in environments {
|
||||
let x = self.upsert_environment(&x, source)?;
|
||||
imported_resources.environments.push(x.clone());
|
||||
}
|
||||
info!("Upserted {} environments", imported_resources.environments.len());
|
||||
}
|
||||
|
||||
// Do folders last so it doesn't cause the UI to render empty folders before populating
|
||||
// immediately after.
|
||||
if folders.len() > 0 {
|
||||
for v in folders {
|
||||
let x = self.upsert_folder(&v, source)?;
|
||||
imported_resources.folders.push(x.clone());
|
||||
}
|
||||
info!("Upserted {} folders", imported_resources.folders.len());
|
||||
}
|
||||
|
||||
Ok(imported_resources)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Error::{MissingBaseEnvironment, MultipleBaseEnvironments};
|
||||
use crate::error::Result;
|
||||
use crate::models::{Environment, EnvironmentIden};
|
||||
use crate::models::{Environment, EnvironmentIden, EnvironmentVariable};
|
||||
use crate::util::UpdateSource;
|
||||
use log::info;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_environment(&self, id: &str) -> Result<Environment> {
|
||||
@@ -10,35 +11,41 @@ impl<'a> DbContext<'a> {
|
||||
}
|
||||
|
||||
pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> {
|
||||
// Will create base environment if it doesn't exist
|
||||
let environments = self.list_environments(workspace_id)?;
|
||||
let environments = self.list_environments_ensure_base(workspace_id)?;
|
||||
let base_environments =
|
||||
environments.into_iter().filter(|e| e.base).collect::<Vec<Environment>>();
|
||||
|
||||
let base_environment = environments
|
||||
.into_iter()
|
||||
.find(|e| e.environment_id == None && e.workspace_id == workspace_id)
|
||||
.ok_or(GenericError(format!("No base environment found for {workspace_id}")))?;
|
||||
if base_environments.len() > 1 {
|
||||
return Err(MultipleBaseEnvironments(workspace_id.to_string()));
|
||||
}
|
||||
|
||||
let base_environment = base_environments.into_iter().find(|e| e.base).ok_or(
|
||||
// Should never happen because one should be created above if it does not exist
|
||||
MissingBaseEnvironment(workspace_id.to_string()),
|
||||
)?;
|
||||
|
||||
Ok(base_environment)
|
||||
}
|
||||
|
||||
pub fn list_environments(&self, workspace_id: &str) -> Result<Vec<Environment>> {
|
||||
/// Lists environments and will create a base environment if one doesn't exist
|
||||
pub fn list_environments_ensure_base(&self, workspace_id: &str) -> Result<Vec<Environment>> {
|
||||
let mut environments =
|
||||
self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?;
|
||||
|
||||
let base_environment = environments
|
||||
.iter()
|
||||
.find(|e| e.environment_id == None && e.workspace_id == workspace_id);
|
||||
let base_environment = environments.iter().find(|e| e.base);
|
||||
|
||||
if let None = base_environment {
|
||||
environments.push(self.upsert_environment(
|
||||
let e = self.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
environment_id: None,
|
||||
base: true,
|
||||
name: "Global Variables".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)?);
|
||||
)?;
|
||||
info!("Created base environment {} for {workspace_id}", e.id);
|
||||
environments.push(e);
|
||||
}
|
||||
|
||||
Ok(environments)
|
||||
@@ -49,12 +56,12 @@ impl<'a> DbContext<'a> {
|
||||
environment: &Environment,
|
||||
source: &UpdateSource,
|
||||
) -> Result<Environment> {
|
||||
for environment in
|
||||
self.find_many::<Environment>(EnvironmentIden::EnvironmentId, &environment.id, None)?
|
||||
{
|
||||
self.delete_environment(&environment, source)?;
|
||||
}
|
||||
self.delete(environment, source)
|
||||
let deleted_environment = self.delete(environment, source)?;
|
||||
|
||||
// Recreate the base environment if we happened to delete it
|
||||
self.list_environments_ensure_base(&environment.workspace_id)?;
|
||||
|
||||
Ok(deleted_environment)
|
||||
}
|
||||
|
||||
pub fn delete_environment_by_id(&self, id: &str, source: &UpdateSource) -> Result<Environment> {
|
||||
@@ -69,7 +76,7 @@ impl<'a> DbContext<'a> {
|
||||
) -> Result<Environment> {
|
||||
let mut environment = environment.clone();
|
||||
environment.id = "".to_string();
|
||||
self.upsert(&environment, source)
|
||||
self.upsert_environment(&environment, source)
|
||||
}
|
||||
|
||||
pub fn upsert_environment(
|
||||
@@ -77,6 +84,18 @@ impl<'a> DbContext<'a> {
|
||||
environment: &Environment,
|
||||
source: &UpdateSource,
|
||||
) -> Result<Environment> {
|
||||
self.upsert(environment, source)
|
||||
let cleaned_variables = environment
|
||||
.variables
|
||||
.iter()
|
||||
.filter(|v| !v.name.is_empty() || !v.value.is_empty())
|
||||
.cloned()
|
||||
.collect::<Vec<EnvironmentVariable>>();
|
||||
self.upsert(
|
||||
&Environment {
|
||||
variables: cleaned_variables,
|
||||
..environment.clone()
|
||||
},
|
||||
source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ use crate::connection_or_tx::ConnectionOrTx;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden,
|
||||
WebsocketRequest, WebsocketRequestIden,
|
||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader,
|
||||
HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_folder(&self, id: &str) -> Result<Folder> {
|
||||
@@ -110,4 +112,40 @@ impl<'a> DbContext<'a> {
|
||||
|
||||
Ok(new_folder)
|
||||
}
|
||||
|
||||
pub fn resolve_auth_for_folder(
|
||||
&self,
|
||||
folder: Folder,
|
||||
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
|
||||
if let Some(at) = folder.authentication_type {
|
||||
return Ok((Some(at), folder.authentication));
|
||||
}
|
||||
|
||||
if let Some(folder_id) = folder.folder_id {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
return self.resolve_auth_for_folder(folder);
|
||||
}
|
||||
|
||||
let workspace = self.get_workspace(&folder.workspace_id)?;
|
||||
Ok(self.resolve_auth_for_workspace(&workspace))
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_folder(&self, folder: &Folder) -> Result<Vec<HttpRequestHeader>> {
|
||||
let mut headers = Vec::new();
|
||||
|
||||
if let Some(folder_id) = folder.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
|
||||
// NOTE: Add parent headers first, so overrides are logical
|
||||
headers.append(&mut folder_headers);
|
||||
} else {
|
||||
let workspace = self.get_workspace(&folder.workspace_id)?;
|
||||
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut folder.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{GrpcRequest, GrpcRequestIden};
|
||||
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_grpc_request(&self, id: &str) -> Result<GrpcRequest> {
|
||||
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
|
||||
) -> Result<GrpcRequest> {
|
||||
self.upsert(grpc_request, source)
|
||||
}
|
||||
|
||||
pub fn resolve_auth_for_grpc_request(
|
||||
&self,
|
||||
grpc_request: &GrpcRequest,
|
||||
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
|
||||
if let Some(at) = grpc_request.authentication_type.clone() {
|
||||
return Ok((Some(at), grpc_request.authentication.clone()));
|
||||
}
|
||||
|
||||
if let Some(folder_id) = grpc_request.folder_id.clone() {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
return self.resolve_auth_for_folder(folder);
|
||||
}
|
||||
|
||||
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
|
||||
Ok(self.resolve_auth_for_workspace(&workspace))
|
||||
}
|
||||
|
||||
pub fn resolve_metadata_for_grpc_request(
|
||||
&self,
|
||||
grpc_request: &GrpcRequest,
|
||||
) -> Result<Vec<HttpRequestHeader>> {
|
||||
// Resolved headers should be from furthest to closest ancestor, to override logically.
|
||||
let mut metadata = Vec::new();
|
||||
|
||||
if let Some(folder_id) = grpc_request.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
|
||||
metadata.append(&mut folder_headers);
|
||||
} else {
|
||||
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
|
||||
let mut workspace_metadata = self.resolve_headers_for_workspace(&workspace);
|
||||
metadata.append(&mut workspace_metadata);
|
||||
}
|
||||
|
||||
metadata.append(&mut grpc_request.metadata.clone());
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{HttpRequest, HttpRequestIden};
|
||||
use crate::models::{HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_http_request(&self, id: &str) -> Result<HttpRequest> {
|
||||
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
|
||||
) -> Result<HttpRequest> {
|
||||
self.upsert(http_request, source)
|
||||
}
|
||||
|
||||
pub fn resolve_auth_for_http_request(
|
||||
&self,
|
||||
http_request: &HttpRequest,
|
||||
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
|
||||
if let Some(at) = http_request.authentication_type.clone() {
|
||||
return Ok((Some(at), http_request.authentication.clone()));
|
||||
}
|
||||
|
||||
if let Some(folder_id) = http_request.folder_id.clone() {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
return self.resolve_auth_for_folder(folder);
|
||||
}
|
||||
|
||||
let workspace = self.get_workspace(&http_request.workspace_id)?;
|
||||
Ok(self.resolve_auth_for_workspace(&workspace))
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_http_request(
|
||||
&self,
|
||||
http_request: &HttpRequest,
|
||||
) -> Result<Vec<HttpRequestHeader>> {
|
||||
// Resolved headers should be from furthest to closest ancestor, to override logically.
|
||||
let mut headers = Vec::new();
|
||||
|
||||
if let Some(folder_id) = http_request.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
|
||||
headers.append(&mut folder_headers);
|
||||
} else {
|
||||
let workspace = self.get_workspace(&http_request.workspace_id)?;
|
||||
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut http_request.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ impl<'a> DbContext<'a> {
|
||||
let (sql, params) = Query::select()
|
||||
.from(KeyValueIden::Table)
|
||||
.column(Asterisk)
|
||||
// Temporary clause to prevent bug when reverting to the previous version, before the
|
||||
// ID column was added. A previous version will not know about ID and will create
|
||||
// key/value entries that don't have one. This clause ensures they are not queried
|
||||
// TODO: Add migration to delete key/values with NULL IDs later on, then remove this
|
||||
.cond_where(Expr::col(KeyValueIden::Id).is_not_null())
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = self.conn.prepare(sql.as_str())?;
|
||||
let items = stmt.query_map(&*params.as_params(), KeyValue::from_row)?;
|
||||
|
||||
@@ -23,6 +23,7 @@ impl<'a> DbContext<'a> {
|
||||
editor_soft_wrap: true,
|
||||
interface_font_size: 15,
|
||||
interface_scale: 1.0,
|
||||
hide_window_controls: false,
|
||||
open_workspace_new_window: None,
|
||||
proxy: None,
|
||||
theme_dark: "yaak-dark".to_string(),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{WebsocketRequest, WebsocketRequestIden};
|
||||
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_websocket_request(&self, id: &str) -> Result<WebsocketRequest> {
|
||||
@@ -48,4 +50,47 @@ impl<'a> DbContext<'a> {
|
||||
) -> Result<WebsocketRequest> {
|
||||
self.upsert(websocket_request, source)
|
||||
}
|
||||
|
||||
pub fn resolve_auth_for_websocket_request(
|
||||
&self,
|
||||
websocket_request: &WebsocketRequest,
|
||||
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
|
||||
if let Some(at) = websocket_request.authentication_type.clone() {
|
||||
return Ok((Some(at), websocket_request.authentication.clone()));
|
||||
}
|
||||
|
||||
if let Some(folder_id) = websocket_request.folder_id.clone() {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
return self.resolve_auth_for_folder(folder);
|
||||
}
|
||||
|
||||
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
|
||||
Ok(self.resolve_auth_for_workspace(&workspace))
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_websocket_request(
|
||||
&self,
|
||||
websocket_request: &WebsocketRequest,
|
||||
) -> Result<Vec<HttpRequestHeader>> {
|
||||
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
|
||||
|
||||
// Resolved headers should be from furthest to closest ancestor, to override logically.
|
||||
let mut headers = Vec::new();
|
||||
|
||||
headers.append(&mut workspace.headers.clone());
|
||||
|
||||
if let Some(folder_id) = websocket_request.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
|
||||
headers.append(&mut folder_headers);
|
||||
} else {
|
||||
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
|
||||
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut websocket_request.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{WorkspaceMeta, WorkspaceMetaIden};
|
||||
use crate::util::UpdateSource;
|
||||
use log::info;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_workspace_meta(&self, workspace_id: &str) -> Option<WorkspaceMeta> {
|
||||
@@ -23,10 +24,7 @@ impl<'a> DbContext<'a> {
|
||||
Ok(workspace_metas)
|
||||
}
|
||||
|
||||
pub fn get_or_create_workspace_meta(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
pub fn get_or_create_workspace_meta(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
|
||||
let workspace_meta = self.get_workspace_meta(workspace_id);
|
||||
if let Some(workspace_meta) = workspace_meta {
|
||||
return Ok(workspace_meta);
|
||||
@@ -37,6 +35,8 @@ impl<'a> DbContext<'a> {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
info!("Creating WorkspaceMeta for {workspace_id}");
|
||||
|
||||
self.upsert_workspace_meta(&workspace_meta, &UpdateSource::Background)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden,
|
||||
WebsocketRequest, WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
|
||||
WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_workspace(&self, id: &str) -> Result<Workspace> {
|
||||
@@ -34,24 +36,26 @@ impl<'a> DbContext<'a> {
|
||||
workspace: &Workspace,
|
||||
source: &UpdateSource,
|
||||
) -> Result<Workspace> {
|
||||
for m in self.find_many::<HttpRequest>(HttpRequestIden::WorkspaceId, &workspace.id, None)? {
|
||||
for m in self.find_many(HttpRequestIden::WorkspaceId, &workspace.id, None)? {
|
||||
self.delete_http_request(&m, source)?;
|
||||
}
|
||||
|
||||
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::WorkspaceId, &workspace.id, None)? {
|
||||
|
||||
for m in self.find_many(GrpcRequestIden::WorkspaceId, &workspace.id, None)? {
|
||||
self.delete_grpc_request(&m, source)?;
|
||||
}
|
||||
|
||||
for m in
|
||||
self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, &workspace.id, None)?
|
||||
{
|
||||
|
||||
for m in self.find_many(WebsocketRequestIden::FolderId, &workspace.id, None)? {
|
||||
self.delete_websocket_request(&m, source)?;
|
||||
}
|
||||
|
||||
for folder in self.find_many::<Folder>(FolderIden::WorkspaceId, &workspace.id, None)? {
|
||||
self.delete_folder(&folder, source)?;
|
||||
|
||||
for m in self.find_many(FolderIden::WorkspaceId, &workspace.id, None)? {
|
||||
self.delete_folder(&m, source)?;
|
||||
}
|
||||
|
||||
|
||||
for m in self.find_many(EnvironmentIden::WorkspaceId, &workspace.id, None)? {
|
||||
self.delete_environment(&m, source)?;
|
||||
}
|
||||
|
||||
self.delete(workspace, source)
|
||||
}
|
||||
|
||||
@@ -63,4 +67,15 @@ impl<'a> DbContext<'a> {
|
||||
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
|
||||
self.upsert(w, source)
|
||||
}
|
||||
|
||||
pub fn resolve_auth_for_workspace(
|
||||
&self,
|
||||
workspace: &Workspace,
|
||||
) -> (Option<String>, BTreeMap<String, Value>) {
|
||||
(workspace.authentication_type.clone(), workspace.authentication.clone())
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
||||
workspace.headers.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::error::Result;
|
||||
use crate::models::{AnyModel, Environment, Folder, GrpcRequest, HttpRequest, UpsertModelInfo, WebsocketRequest, Workspace, WorkspaceIden};
|
||||
use crate::models::{
|
||||
AnyModel, Environment, Folder, GrpcRequest, HttpRequest, UpsertModelInfo, WebsocketRequest,
|
||||
Workspace, WorkspaceIden,
|
||||
};
|
||||
use crate::query_manager::QueryManagerExt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use log::warn;
|
||||
@@ -117,14 +120,14 @@ pub struct BatchUpsertResult {
|
||||
pub websocket_requests: Vec<WebsocketRequest>,
|
||||
}
|
||||
|
||||
pub async fn get_workspace_export_resources<R: Runtime>(
|
||||
pub fn get_workspace_export_resources<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
workspace_ids: Vec<&str>,
|
||||
include_environments: bool,
|
||||
include_private_environments: bool,
|
||||
) -> Result<WorkspaceExport> {
|
||||
let mut data = WorkspaceExport {
|
||||
yaak_version: app_handle.package_info().version.clone().to_string(),
|
||||
yaak_schema: 3,
|
||||
yaak_schema: 4,
|
||||
timestamp: Utc::now().naive_utc(),
|
||||
resources: BatchUpsertResult {
|
||||
workspaces: Vec::new(),
|
||||
@@ -139,18 +142,19 @@ pub async fn get_workspace_export_resources<R: Runtime>(
|
||||
let db = app_handle.db();
|
||||
for workspace_id in workspace_ids {
|
||||
data.resources.workspaces.push(db.find_one(WorkspaceIden::Id, workspace_id)?);
|
||||
data.resources.environments.append(&mut db.list_environments(workspace_id)?);
|
||||
data.resources.environments.append(
|
||||
&mut db
|
||||
.list_environments_ensure_base(workspace_id)?
|
||||
.into_iter()
|
||||
.filter(|e| include_private_environments || e.public)
|
||||
.collect(),
|
||||
);
|
||||
data.resources.folders.append(&mut db.list_folders(workspace_id)?);
|
||||
data.resources.http_requests.append(&mut db.list_http_requests(workspace_id)?);
|
||||
data.resources.grpc_requests.append(&mut db.list_grpc_requests(workspace_id)?);
|
||||
data.resources.websocket_requests.append(&mut db.list_websocket_requests(workspace_id)?);
|
||||
}
|
||||
|
||||
// Nuke environments if we don't want them
|
||||
if !include_environments {
|
||||
data.resources.environments.clear();
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputCheckbox = {
|
||||
/**
|
||||
@@ -131,7 +135,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputEditor = {
|
||||
/**
|
||||
@@ -170,7 +178,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputFile = {
|
||||
/**
|
||||
@@ -205,7 +217,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
@@ -232,7 +248,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||
|
||||
@@ -265,7 +285,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputSelectOption = { label: string, value: string, };
|
||||
|
||||
@@ -306,10 +330,18 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
|
||||
|
||||
export type GetCookieValueRequest = { name: string, };
|
||||
|
||||
export type GetCookieValueResponse = { value: string | null, };
|
||||
|
||||
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
|
||||
|
||||
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
|
||||
@@ -346,10 +378,14 @@ export type ImportResponse = { resources: ImportResources, };
|
||||
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
export type ListCookieNamesRequest = {};
|
||||
|
||||
export type ListCookieNamesResponse = { names: Array<string>, };
|
||||
|
||||
export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* Label for the window. If not provided, a random one will be generated.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -84,6 +84,11 @@ pub enum InternalEventPayload {
|
||||
SendHttpRequestRequest(SendHttpRequestRequest),
|
||||
SendHttpRequestResponse(SendHttpRequestResponse),
|
||||
|
||||
ListCookieNamesRequest(ListCookieNamesRequest),
|
||||
ListCookieNamesResponse(ListCookieNamesResponse),
|
||||
GetCookieValueRequest(GetCookieValueRequest),
|
||||
GetCookieValueResponse(GetCookieValueResponse),
|
||||
|
||||
// Request Actions
|
||||
GetHttpRequestActionsRequest(EmptyPayload),
|
||||
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
|
||||
@@ -231,6 +236,32 @@ pub struct SendHttpRequestResponse {
|
||||
pub http_response: HttpResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default)]
|
||||
#[ts(export, type = "{}", export_to = "gen_events.ts")]
|
||||
pub struct ListCookieNamesRequest {}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct ListCookieNamesResponse {
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct GetCookieValueRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct GetCookieValueResponse {
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
@@ -563,6 +594,10 @@ pub struct FormInputBase {
|
||||
|
||||
#[ts(optional)]
|
||||
pub disabled: Option<bool>,
|
||||
|
||||
/// Longer description of the input, likely shown in a tooltip
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
@@ -805,7 +840,7 @@ pub struct CallTemplateFunctionResponse {
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct CallTemplateFunctionArgs {
|
||||
pub purpose: RenderPurpose,
|
||||
pub values: HashMap<String, String>,
|
||||
pub values: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::events::{
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse,
|
||||
InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
|
||||
};
|
||||
use crate::native_template_functions::template_function_secure;
|
||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||
use crate::plugin_handle::PluginHandle;
|
||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||
@@ -24,13 +25,12 @@ use tauri::path::BaseDirectory;
|
||||
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::time::{timeout, Instant};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio::time::{Instant, timeout};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::generate_id;
|
||||
use yaak_templates::error::Error::RenderError;
|
||||
use yaak_templates::error::Result as TemplateResult;
|
||||
use crate::native_template_functions::template_function_secure;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginManager {
|
||||
@@ -160,8 +160,7 @@ impl PluginManager {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let plugins =
|
||||
app_handle.db().list_plugins().unwrap_or_default();
|
||||
let plugins = app_handle.db().list_plugins().unwrap_or_default();
|
||||
let installed_plugin_dirs: Vec<PluginCandidate> = plugins
|
||||
.iter()
|
||||
.map(|p| PluginCandidate {
|
||||
@@ -537,7 +536,7 @@ impl PluginManager {
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
request_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<()> {
|
||||
let results = self.get_http_authentication_summaries(window).await?;
|
||||
let plugin = results
|
||||
@@ -545,7 +544,7 @@ impl PluginManager {
|
||||
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
|
||||
.ok_or(PluginNotFoundErr(auth_name.into()))?;
|
||||
|
||||
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
|
||||
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
|
||||
self.send_to_plugin_and_wait(
|
||||
&PluginWindowContext::new(window),
|
||||
&plugin,
|
||||
@@ -606,15 +605,12 @@ impl PluginManager {
|
||||
&self,
|
||||
window_context: &PluginWindowContext,
|
||||
fn_name: &str,
|
||||
args: HashMap<String, String>,
|
||||
values: HashMap<String, serde_json::Value>,
|
||||
purpose: RenderPurpose,
|
||||
) -> TemplateResult<String> {
|
||||
let req = CallTemplateFunctionRequest {
|
||||
name: fn_name.to_string(),
|
||||
args: CallTemplateFunctionArgs {
|
||||
purpose,
|
||||
values: args,
|
||||
},
|
||||
args: CallTemplateFunctionArgs { purpose, values },
|
||||
};
|
||||
|
||||
let events = self
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(crate) fn template_function_secure() -> TemplateFunction {
|
||||
|
||||
pub fn template_function_secure_run<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
args: HashMap<String, String>,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
window_context: &PluginWindowContext,
|
||||
) -> Result<String> {
|
||||
match window_context.clone() {
|
||||
@@ -43,9 +43,10 @@ pub fn template_function_secure_run<R: Runtime>(
|
||||
..
|
||||
} => {
|
||||
let value = args.get("value").map(|v| v.to_owned()).unwrap_or_default();
|
||||
if value.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
let value = match value {
|
||||
serde_json::Value::String(s) => s,
|
||||
_ => return Ok("".to_string()),
|
||||
};
|
||||
|
||||
let value = match value.strip_prefix("YENC_") {
|
||||
None => {
|
||||
@@ -118,7 +119,7 @@ pub fn decrypt_secure_template_function<R: Runtime>(
|
||||
for a in args {
|
||||
match a.clone().value {
|
||||
Val::Str { text } => {
|
||||
args_map.insert(a.name.to_string(), text);
|
||||
args_map.insert(a.name.to_string(), serde_json::Value::String(text));
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl<R: Runtime> PluginTemplateCallback<R> {
|
||||
}
|
||||
|
||||
impl<R: Runtime> TemplateCallback for PluginTemplateCallback<R> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
let fn_name = if fn_name == "Response" { "response" } else { fn_name };
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -22,4 +20,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -4,4 +4,4 @@ import type { SyncState } from "./gen_models.js";
|
||||
|
||||
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };
|
||||
|
||||
export type SyncOp = { "type": "fsCreate", model: SyncModel, } | { "type": "fsUpdate", model: SyncModel, state: SyncState, } | { "type": "fsDelete", state: SyncState, fs: FsCandidate | null, } | { "type": "dbCreate", fs: FsCandidate, } | { "type": "dbUpdate", state: SyncState, fs: FsCandidate, } | { "type": "dbDelete", model: SyncModel, state: SyncState, };
|
||||
export type SyncOp = { "type": "fsCreate", model: SyncModel, } | { "type": "fsUpdate", model: SyncModel, state: SyncState, } | { "type": "fsDelete", state: SyncState, fs: FsCandidate | null, } | { "type": "dbCreate", fs: FsCandidate, } | { "type": "dbUpdate", state: SyncState, fs: FsCandidate, } | { "type": "dbDelete", model: SyncModel, state: SyncState, } | { "type": "ignorePrivate", model: SyncModel, };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates, get_fs_candidates,
|
||||
FsCandidate, SyncOp,
|
||||
apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates, get_fs_candidates, FsCandidate,
|
||||
SyncOp,
|
||||
};
|
||||
use crate::watch::{watch_directory, WatchEvent};
|
||||
use chrono::Utc;
|
||||
@@ -19,9 +19,8 @@ pub async fn calculate<R: Runtime>(
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
) -> Result<Vec<SyncOp>> {
|
||||
let db_candidates = get_db_candidates(&app_handle, workspace_id, sync_dir).await?;
|
||||
let fs_candidates = get_fs_candidates(sync_dir)
|
||||
.await?
|
||||
let db_candidates = get_db_candidates(&app_handle, workspace_id, sync_dir)?;
|
||||
let fs_candidates = get_fs_candidates(sync_dir)?
|
||||
.into_iter()
|
||||
// Only keep items in the same workspace
|
||||
.filter(|fs| fs.model.workspace_id() == workspace_id)
|
||||
@@ -34,7 +33,7 @@ pub async fn calculate<R: Runtime>(
|
||||
#[command]
|
||||
pub async fn calculate_fs(dir: &Path) -> Result<Vec<SyncOp>> {
|
||||
let db_candidates = Vec::new();
|
||||
let fs_candidates = get_fs_candidates(dir).await?;
|
||||
let fs_candidates = get_fs_candidates(dir)?;
|
||||
Ok(compute_sync_ops(db_candidates, fs_candidates))
|
||||
}
|
||||
|
||||
@@ -45,8 +44,8 @@ pub async fn apply<R: Runtime>(
|
||||
sync_dir: &Path,
|
||||
workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
let sync_state_ops = apply_sync_ops(&app_handle, &workspace_id, sync_dir, sync_ops).await?;
|
||||
apply_sync_state_ops(&app_handle, workspace_id, sync_dir, sync_state_ops).await
|
||||
let sync_state_ops = apply_sync_ops(&app_handle, &workspace_id, sync_dir, sync_ops)?;
|
||||
apply_sync_state_ops(&app_handle, workspace_id, sync_dir, sync_state_ops)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -130,4 +130,4 @@ impl TryFrom<AnyModel> for SyncModel {
|
||||
};
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::error::Result;
|
||||
use crate::models::SyncModel;
|
||||
use chrono::Utc;
|
||||
use log::{debug, info, warn};
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tokio::fs;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use ts_rs::TS;
|
||||
use yaak_models::models::{SyncState, WorkspaceMeta};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
@@ -41,17 +41,21 @@ pub(crate) enum SyncOp {
|
||||
model: SyncModel,
|
||||
state: SyncState,
|
||||
},
|
||||
IgnorePrivate {
|
||||
model: SyncModel,
|
||||
},
|
||||
}
|
||||
|
||||
impl SyncOp {
|
||||
fn workspace_id(&self) -> String {
|
||||
match self {
|
||||
SyncOp::FsCreate { model } => model.workspace_id(),
|
||||
SyncOp::FsUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::FsDelete { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::DbCreate { fs } => fs.model.workspace_id(),
|
||||
SyncOp::DbUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::DbDelete { model, .. } => model.workspace_id(),
|
||||
SyncOp::DbUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::FsCreate { model } => model.workspace_id(),
|
||||
SyncOp::FsDelete { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::FsUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::IgnorePrivate { model } => model.workspace_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +70,7 @@ impl Display for SyncOp {
|
||||
SyncOp::DbCreate { fs } => format!("db_create({})", fs.model.id()),
|
||||
SyncOp::DbUpdate { fs, .. } => format!("db_update({})", fs.model.id()),
|
||||
SyncOp::DbDelete { model, .. } => format!("db_delete({})", model.id()),
|
||||
SyncOp::IgnorePrivate { model } => format!("ignore_private({})", model.id()),
|
||||
}
|
||||
.as_str(),
|
||||
)
|
||||
@@ -76,8 +81,8 @@ impl Display for SyncOp {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum DbCandidate {
|
||||
Added(SyncModel),
|
||||
Modified(SyncModel, SyncState),
|
||||
Deleted(SyncState),
|
||||
Modified(SyncModel, SyncState),
|
||||
Unmodified(SyncModel, SyncState),
|
||||
}
|
||||
|
||||
@@ -85,8 +90,8 @@ impl DbCandidate {
|
||||
fn model_id(&self) -> String {
|
||||
match &self {
|
||||
DbCandidate::Added(m) => m.id(),
|
||||
DbCandidate::Modified(m, _) => m.id(),
|
||||
DbCandidate::Deleted(s) => s.model_id.clone(),
|
||||
DbCandidate::Modified(m, _) => m.id(),
|
||||
DbCandidate::Unmodified(m, _) => m.id(),
|
||||
}
|
||||
}
|
||||
@@ -101,16 +106,13 @@ pub(crate) struct FsCandidate {
|
||||
pub(crate) checksum: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn get_db_candidates<R: Runtime>(
|
||||
pub(crate) fn get_db_candidates<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
) -> Result<Vec<DbCandidate>> {
|
||||
let models: HashMap<_, _> = workspace_models(app_handle, workspace_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| (m.id(), m))
|
||||
.collect();
|
||||
let models: HashMap<_, _> =
|
||||
workspace_models(app_handle, workspace_id)?.into_iter().map(|m| (m.id(), m)).collect();
|
||||
let sync_states: HashMap<_, _> = app_handle
|
||||
.db()
|
||||
.list_sync_states_for_workspace(workspace_id, sync_dir)?
|
||||
@@ -121,20 +123,42 @@ pub(crate) async fn get_db_candidates<R: Runtime>(
|
||||
// 1. Add candidates for models (created/modified/unmodified)
|
||||
let mut candidates: Vec<DbCandidate> = models
|
||||
.values()
|
||||
.map(|model| {
|
||||
let existing_sync_state = match sync_states.get(&model.id()) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
// No sync state yet, so model was just added
|
||||
return DbCandidate::Added(model.to_owned());
|
||||
}
|
||||
};
|
||||
.filter_map(|model| {
|
||||
match sync_states.get(&model.id()) {
|
||||
Some(existing_sync_state) => {
|
||||
// If a sync state exists but the model is now private, treat it as a deletion
|
||||
match model {
|
||||
SyncModel::Environment(e) if !e.public => {
|
||||
return Some(DbCandidate::Deleted(existing_sync_state.to_owned()));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let updated_since_flush = model.updated_at() > existing_sync_state.flushed_at;
|
||||
if updated_since_flush {
|
||||
DbCandidate::Modified(model.to_owned(), existing_sync_state.to_owned())
|
||||
} else {
|
||||
DbCandidate::Unmodified(model.to_owned(), existing_sync_state.to_owned())
|
||||
let updated_since_flush = model.updated_at() > existing_sync_state.flushed_at;
|
||||
if updated_since_flush {
|
||||
Some(DbCandidate::Modified(
|
||||
model.to_owned(),
|
||||
existing_sync_state.to_owned(),
|
||||
))
|
||||
} else {
|
||||
Some(DbCandidate::Unmodified(
|
||||
model.to_owned(),
|
||||
existing_sync_state.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return match model {
|
||||
SyncModel::Environment(e) if !e.public => {
|
||||
// No sync state yet, so ignore the model
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
// No sync state yet, so the model was just added
|
||||
Some(DbCandidate::Added(model.to_owned()))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -151,30 +175,28 @@ pub(crate) async fn get_db_candidates<R: Runtime>(
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
|
||||
pub(crate) fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
|
||||
// Ensure the root directory exists
|
||||
fs::create_dir_all(dir).await?;
|
||||
fs::create_dir_all(dir)?;
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
let mut entries = fs::read_dir(dir).await?;
|
||||
while let Some(dir_entry) = entries.next_entry().await? {
|
||||
if !dir_entry.file_type().await?.is_file() {
|
||||
let entries = fs::read_dir(dir)?;
|
||||
for dir_entry in entries {
|
||||
let dir_entry = match dir_entry {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if !dir_entry.file_type()?.is_file() {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = dir_entry.path();
|
||||
let (model, checksum) = match SyncModel::from_file(&path) {
|
||||
// TODO: Remove this once we have logic to handle environments. This it to clean
|
||||
// any existing ones from the sync dir that resulted from the 2025.1 betas.
|
||||
Ok(Some((SyncModel::Environment(e), _))) => {
|
||||
fs::remove_file(path).await?;
|
||||
info!("Cleaned up synced environment {}", e.id);
|
||||
continue;
|
||||
}
|
||||
Ok(Some(m)) => m,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
warn!("Failed to read sync file {e}");
|
||||
warn!("Failed to parse sync file {e}");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -212,32 +234,32 @@ pub(crate) fn compute_sync_ops(
|
||||
let op = match (db_map.get(k), fs_map.get(k)) {
|
||||
(None, None) => return None, // Can never happen
|
||||
(None, Some(fs)) => SyncOp::DbCreate { fs: fs.to_owned() },
|
||||
(Some(DbCandidate::Unmodified(model, sync_state)), None) => {
|
||||
// TODO: Remove this once we have logic to handle environments. This it to
|
||||
// ignore the cleaning we did above of any environments that were written
|
||||
// to disk in the 2025.1 betas.
|
||||
if let SyncModel::Environment(_) = model {
|
||||
return None;
|
||||
}
|
||||
SyncOp::DbDelete {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
// DB unchanged <-> FS missing
|
||||
(Some(DbCandidate::Unmodified(model, sync_state)), None) => SyncOp::DbDelete {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
},
|
||||
|
||||
// DB modified <-> FS missing
|
||||
(Some(DbCandidate::Modified(model, sync_state)), None) => SyncOp::FsUpdate {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
},
|
||||
|
||||
// DB added <-> FS missing
|
||||
(Some(DbCandidate::Added(model)), None) => SyncOp::FsCreate {
|
||||
model: model.to_owned(),
|
||||
},
|
||||
(Some(DbCandidate::Deleted(sync_state)), None) => {
|
||||
// Already deleted on FS, but sending it so the SyncState gets dealt with
|
||||
SyncOp::FsDelete {
|
||||
state: sync_state.to_owned(),
|
||||
fs: None,
|
||||
}
|
||||
}
|
||||
|
||||
// DB deleted <-> FS missing
|
||||
// Already deleted on FS, but sending it so the SyncState gets dealt with
|
||||
(Some(DbCandidate::Deleted(sync_state)), None) => SyncOp::FsDelete {
|
||||
state: sync_state.to_owned(),
|
||||
fs: None,
|
||||
},
|
||||
|
||||
// DB unchanged <-> FS exists
|
||||
(Some(DbCandidate::Unmodified(_, sync_state)), Some(fs_candidate)) => {
|
||||
if sync_state.checksum == fs_candidate.checksum {
|
||||
return None;
|
||||
@@ -248,6 +270,8 @@ pub(crate) fn compute_sync_ops(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DB modified <-> FS exists
|
||||
(Some(DbCandidate::Modified(model, sync_state)), Some(fs_candidate)) => {
|
||||
if sync_state.checksum == fs_candidate.checksum {
|
||||
SyncOp::FsUpdate {
|
||||
@@ -255,25 +279,29 @@ pub(crate) fn compute_sync_ops(
|
||||
state: sync_state.to_owned(),
|
||||
}
|
||||
} else if model.updated_at() < fs_candidate.model.updated_at() {
|
||||
// CONFLICT! Write to DB if fs model is newer
|
||||
// CONFLICT! Write to DB if the fs model is newer
|
||||
SyncOp::DbUpdate {
|
||||
state: sync_state.to_owned(),
|
||||
fs: fs_candidate.to_owned(),
|
||||
}
|
||||
} else {
|
||||
// CONFLICT! Write to FS if db model is newer
|
||||
// CONFLICT! Write to FS if the db model is newer
|
||||
SyncOp::FsUpdate {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DB added <-> FS anything
|
||||
(Some(DbCandidate::Added(model)), Some(_)) => {
|
||||
// This would be super rare (impossible?), so let's follow the user's intention
|
||||
SyncOp::FsCreate {
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
// DB deleted <-> FS exists
|
||||
(Some(DbCandidate::Deleted(sync_state)), Some(fs_candidate)) => SyncOp::FsDelete {
|
||||
state: sync_state.to_owned(),
|
||||
fs: Some(fs_candidate.to_owned()),
|
||||
@@ -284,12 +312,19 @@ pub(crate) fn compute_sync_ops(
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn workspace_models<R: Runtime>(
|
||||
fn workspace_models<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
workspace_id: &str,
|
||||
) -> Result<Vec<SyncModel>> {
|
||||
let resources =
|
||||
get_workspace_export_resources(app_handle, vec![workspace_id], true).await?.resources;
|
||||
// We want to include private environments here so that we can take them into account during
|
||||
// the sync process. Otherwise, they would be treated as deleted.
|
||||
let include_private_environments = true;
|
||||
let resources = get_workspace_export_resources(
|
||||
app_handle,
|
||||
vec![workspace_id],
|
||||
include_private_environments,
|
||||
)?
|
||||
.resources;
|
||||
let workspace = resources.workspaces.iter().find(|w| w.id == workspace_id);
|
||||
|
||||
let workspace = match workspace {
|
||||
@@ -318,7 +353,7 @@ async fn workspace_models<R: Runtime>(
|
||||
Ok(sync_models)
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
pub(crate) fn apply_sync_ops<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
@@ -328,13 +363,14 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
debug!(
|
||||
info!(
|
||||
"Applying sync ops {}",
|
||||
sync_ops.iter().map(|op| op.to_string()).collect::<Vec<String>>().join(", ")
|
||||
);
|
||||
|
||||
let mut sync_state_ops = Vec::new();
|
||||
let mut workspaces_to_upsert = Vec::new();
|
||||
let environments_to_upsert = Vec::new();
|
||||
let mut environments_to_upsert = Vec::new();
|
||||
let mut folders_to_upsert = Vec::new();
|
||||
let mut http_requests_to_upsert = Vec::new();
|
||||
let mut grpc_requests_to_upsert = Vec::new();
|
||||
@@ -351,8 +387,8 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
let rel_path = derive_model_filename(&model);
|
||||
let abs_path = sync_dir.join(rel_path.clone());
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
let mut f = File::create(&abs_path)?;
|
||||
f.write_all(&content)?;
|
||||
SyncStateOp::Create {
|
||||
model_id: model.id(),
|
||||
checksum,
|
||||
@@ -364,8 +400,8 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
let mut f = File::create(&abs_path)?;
|
||||
f.write_all(&content)?;
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum,
|
||||
@@ -383,7 +419,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
// Always delete the existing path
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
fs::remove_file(&abs_path).await?;
|
||||
fs::remove_file(&abs_path)?;
|
||||
SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
}
|
||||
@@ -395,14 +431,12 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
// Push updates to arrays so we can do them all in a single
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
|
||||
|
||||
// TODO: Handle environments in sync
|
||||
SyncModel::Environment(_) => {}
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
};
|
||||
SyncStateOp::Create {
|
||||
model_id,
|
||||
@@ -414,14 +448,12 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
// Push updates to arrays so we can do them all in a single
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
|
||||
|
||||
// TODO: Handle environments in sync
|
||||
SyncModel::Environment(_) => {}
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
}
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
@@ -435,6 +467,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
state: state.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::IgnorePrivate { .. } => SyncStateOp::NoOp,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,7 +481,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
websocket_requests_to_upsert,
|
||||
&UpdateSource::Sync,
|
||||
)?;
|
||||
|
||||
|
||||
// Ensure we create WorkspaceMeta models for each new workspace, with the appropriate sync dir
|
||||
let sync_dir_string = sync_dir.to_string_lossy().to_string();
|
||||
for workspace in upserted_models.workspaces {
|
||||
@@ -497,9 +530,10 @@ pub(crate) enum SyncStateOp {
|
||||
Delete {
|
||||
state: SyncState,
|
||||
},
|
||||
NoOp,
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_sync_state_ops<R: Runtime>(
|
||||
pub(crate) fn apply_sync_state_ops<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
@@ -540,6 +574,9 @@ pub(crate) async fn apply_sync_state_ops<R: Runtime>(
|
||||
SyncStateOp::Delete { state } => {
|
||||
app_handle.db().delete_sync_state(&state)?;
|
||||
}
|
||||
SyncStateOp::NoOp => {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,9 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false # Causes errors in CI (haven't figured out why yet)
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ pub trait TemplateCallback {
|
||||
fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
args: HashMap<String, String>,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
) -> impl Future<Output = Result<String>> + Send;
|
||||
|
||||
fn transform_arg(&self, fn_name: &str, arg_name: &str, arg_value: &str) -> Result<String>;
|
||||
@@ -107,9 +107,15 @@ async fn render_value<T: TemplateCallback>(
|
||||
None => return Err(VariableNotFound(name)),
|
||||
},
|
||||
Val::Fn { name, args } => {
|
||||
let mut resolved_args: HashMap<String, String> = HashMap::new();
|
||||
let mut resolved_args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
for a in args {
|
||||
let v = Box::pin(render_value(a.value, vars, cb, depth)).await?;
|
||||
let v = match a.value.clone() {
|
||||
Val::Bool { value } => serde_json::Value::Bool(value),
|
||||
Val::Null => serde_json::Value::Null,
|
||||
_ => serde_json::Value::String(
|
||||
Box::pin(render_value(a.value, vars, cb, depth)).await?,
|
||||
),
|
||||
};
|
||||
resolved_args.insert(a.name, v);
|
||||
}
|
||||
let result = cb.run(name.as_str(), resolved_args.clone()).await?;
|
||||
@@ -133,7 +139,11 @@ mod parse_and_render_tests {
|
||||
struct EmptyCB {}
|
||||
|
||||
impl TemplateCallback for EmptyCB {
|
||||
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(
|
||||
&self,
|
||||
_fn_name: &str,
|
||||
_args: HashMap<String, serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -236,7 +246,11 @@ mod parse_and_render_tests {
|
||||
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
Ok(format!("{fn_name}: {}, {:?} {:?}", args.len(), args.get("a"), args.get("b")))
|
||||
}
|
||||
|
||||
@@ -260,7 +274,11 @@ mod parse_and_render_tests {
|
||||
let result = r#"BAR"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
@@ -290,7 +308,7 @@ mod parse_and_render_tests {
|
||||
let result = r#"FOO 'BAR' BAZ"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
_ => "".to_string(),
|
||||
@@ -319,7 +337,7 @@ mod parse_and_render_tests {
|
||||
let result = r#"BAR"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
@@ -349,7 +367,7 @@ mod parse_and_render_tests {
|
||||
let result = r#"bar"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"no_op" => args["inner"].to_string(),
|
||||
_ => "".to_string(),
|
||||
@@ -377,7 +395,7 @@ mod parse_and_render_tests {
|
||||
let result = r#"ABC"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
@@ -406,7 +424,7 @@ mod parse_and_render_tests {
|
||||
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, _fn_name: &str, _args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
Err(RenderError("Failed to do it!".to_string()))
|
||||
}
|
||||
|
||||
@@ -438,7 +456,7 @@ mod render_json_value_raw_tests {
|
||||
struct EmptyCB {}
|
||||
|
||||
impl TemplateCallback for EmptyCB {
|
||||
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
|
||||
async fn run(&self, _fn_name: &str, _args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::WebsocketManager;
|
||||
use crate::render::render_request;
|
||||
use crate::render::render_websocket_request;
|
||||
use crate::resolve::resolve_websocket_request;
|
||||
use log::{info, warn};
|
||||
use std::str::FromStr;
|
||||
use tauri::http::{HeaderMap, HeaderName};
|
||||
use tauri::{AppHandle, Runtime, State, Url, WebviewWindow};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use yaak_http::apply_path_placeholders;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::models::{
|
||||
HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent,
|
||||
WebsocketEventType, WebsocketRequest,
|
||||
};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{
|
||||
CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose,
|
||||
@@ -119,8 +120,9 @@ pub(crate) async fn send<R: Runtime>(
|
||||
};
|
||||
let base_environment =
|
||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
||||
let request = render_request(
|
||||
&unrendered_request,
|
||||
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
|
||||
let request = render_websocket_request(
|
||||
&resolved_request,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
&PluginTemplateCallback::new(
|
||||
@@ -194,8 +196,9 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
};
|
||||
let base_environment =
|
||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
||||
let request = render_request(
|
||||
&unrendered_request,
|
||||
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
|
||||
let request = render_websocket_request(
|
||||
&resolved_request,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
&PluginTemplateCallback::new(
|
||||
@@ -207,30 +210,55 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
.await?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(auth_name) = request.authentication_type.clone() {
|
||||
let auth = request.authentication.clone();
|
||||
let plugin_req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request_id.to_string())),
|
||||
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
||||
method: "POST".to_string(),
|
||||
url: request.url.clone(),
|
||||
headers: request
|
||||
.headers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|h| HttpHeader {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let plugin_result =
|
||||
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
|
||||
for header in plugin_result.set_headers {
|
||||
headers.insert(
|
||||
HeaderName::from_str(&header.name).unwrap(),
|
||||
HeaderValue::from_str(&header.value).unwrap(),
|
||||
);
|
||||
|
||||
for h in request.headers.clone() {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !h.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.insert(
|
||||
HeaderName::from_str(&h.name).unwrap(),
|
||||
HeaderValue::from_str(&h.value).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
match request.authentication_type {
|
||||
None => {
|
||||
// No authentication found. Not even inherited
|
||||
}
|
||||
Some(authentication_type) if authentication_type == "none" => {
|
||||
// Explicitly no authentication
|
||||
}
|
||||
Some(authentication_type) => {
|
||||
let auth = request.authentication.clone();
|
||||
let plugin_req = CallHttpAuthenticationRequest {
|
||||
context_id: format!("{:x}", md5::compute(request_id.to_string())),
|
||||
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
||||
method: "POST".to_string(),
|
||||
url: request.url.clone(),
|
||||
headers: request
|
||||
.headers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|h| HttpHeader {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let plugin_result = plugin_manager
|
||||
.call_http_authentication(&window, &authentication_type, plugin_req)
|
||||
.await?;
|
||||
for header in plugin_result.set_headers {
|
||||
headers.insert(
|
||||
HeaderName::from_str(&header.name).unwrap(),
|
||||
HeaderValue::from_str(&header.value).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,12 +285,16 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
// Add URL parameters to URL
|
||||
let mut url = Url::parse(&url).unwrap();
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in url_parameters.clone() {
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
let valid_query_pairs = url_parameters
|
||||
.into_iter()
|
||||
.filter(|p| p.enabled && !p.name.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
// NOTE: Only mutate query pairs if there are any, or it will append an empty `?` to the URL
|
||||
if !valid_query_pairs.is_empty() {
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in valid_query_pairs {
|
||||
query_pairs.append_pair(p.name.as_str(), p.value.as_str());
|
||||
}
|
||||
query_pairs.append_pair(p.name.as_str(), p.value.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ mod connect;
|
||||
pub mod error;
|
||||
mod manager;
|
||||
mod render;
|
||||
mod resolve;
|
||||
|
||||
use crate::commands::{
|
||||
close, connect, delete_connection, delete_connections, delete_request, duplicate_request,
|
||||
|
||||
@@ -4,7 +4,7 @@ use yaak_models::models::{Environment, HttpRequestHeader, WebsocketRequest};
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
|
||||
|
||||
pub async fn render_request<T: TemplateCallback>(
|
||||
pub async fn render_websocket_request<T: TemplateCallback>(
|
||||
r: &WebsocketRequest,
|
||||
base_environment: &Environment,
|
||||
environment: Option<&Environment>,
|
||||
|
||||
21
src-tauri/yaak-ws/src/resolve.rs
Normal file
21
src-tauri/yaak-ws/src/resolve.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::error::Result;
|
||||
use tauri::{Runtime, WebviewWindow};
|
||||
use yaak_models::models::WebsocketRequest;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
|
||||
pub(crate) fn resolve_websocket_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &WebsocketRequest,
|
||||
) -> Result<WebsocketRequest> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication) =
|
||||
window.db().resolve_auth_for_websocket_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let headers = window.db().resolve_headers_for_websocket_request(request)?;
|
||||
new_request.headers = headers;
|
||||
|
||||
Ok(new_request)
|
||||
}
|
||||
51
src-web/commands/createEnvironment.ts
Normal file
51
src-web/commands/createEnvironment.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createWorkspaceModel, type Environment } from '@yaakapp-internal/models';
|
||||
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||
|
||||
export const createEnvironmentAndActivate = createFastMutation<
|
||||
string | null,
|
||||
unknown,
|
||||
Environment | null
|
||||
>({
|
||||
mutationKey: ['create_environment'],
|
||||
mutationFn: async (baseEnvironment) => {
|
||||
if (baseEnvironment == null) {
|
||||
throw new Error('No base environment passed');
|
||||
}
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (workspaceId == null) {
|
||||
throw new Error('Cannot create environment when no active workspace');
|
||||
}
|
||||
|
||||
const name = await showPrompt({
|
||||
id: 'new-environment',
|
||||
title: 'New Environment',
|
||||
description: 'Create multiple environments with different sets of variables',
|
||||
label: 'Name',
|
||||
placeholder: 'My Environment',
|
||||
defaultValue: 'My Environment',
|
||||
confirmText: 'Create',
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
return createWorkspaceModel({
|
||||
model: 'environment',
|
||||
name,
|
||||
variables: [],
|
||||
workspaceId,
|
||||
base: false,
|
||||
});
|
||||
},
|
||||
onSuccess: async (environmentId) => {
|
||||
if (environmentId == null) {
|
||||
return; // Was not created
|
||||
}
|
||||
|
||||
console.log('NAVIGATING', jotaiStore.get(activeWorkspaceIdAtom), environmentId);
|
||||
setWorkspaceSearchParams({ environment_id: environmentId });
|
||||
},
|
||||
});
|
||||
14
src-web/commands/openFolderSettings.tsx
Normal file
14
src-web/commands/openFolderSettings.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
|
||||
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
|
||||
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
|
||||
showDialog({
|
||||
id: 'folder-settings',
|
||||
title: 'Folder Settings',
|
||||
size: 'lg',
|
||||
className: 'h-[50rem]',
|
||||
noPadding: true,
|
||||
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SettingsTab } from '../components/Settings/SettingsTab';
|
||||
import type { SettingsTab } from '../components/Settings/Settings';
|
||||
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
@@ -14,8 +14,9 @@ export const openSettings = createFastMutation<void, string, SettingsTab | null>
|
||||
const location = router.buildLocation({
|
||||
to: '/workspaces/$workspaceId/settings',
|
||||
params: { workspaceId },
|
||||
search: { tab: tab ?? SettingsTab.General },
|
||||
search: { tab },
|
||||
});
|
||||
|
||||
await invokeCmd('cmd_new_child_window', {
|
||||
url: location.href,
|
||||
label: 'settings',
|
||||
@@ -11,6 +11,7 @@ export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
|
||||
const workspace = ops
|
||||
.map((o) => (o.type === 'dbCreate' && o.fs.model.type === 'workspace' ? o.fs.model : null))
|
||||
.filter((m) => m)[0];
|
||||
|
||||
if (workspace == null) {
|
||||
showSimpleAlert('Failed to Open', 'No workspace found in directory');
|
||||
return;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog';
|
||||
import type {
|
||||
WorkspaceSettingsTab} from '../components/WorkspaceSettingsDialog';
|
||||
import {
|
||||
WorkspaceSettingsDialog
|
||||
} from '../components/WorkspaceSettingsDialog';
|
||||
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
|
||||
export const openWorkspaceSettings = createFastMutation<void, string>({
|
||||
mutationKey: ['open_workspace_settings'],
|
||||
async mutationFn() {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
showDialog({
|
||||
id: 'workspace-settings',
|
||||
title: 'Workspace Settings',
|
||||
size: 'md',
|
||||
render({ hide }) {
|
||||
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} />;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
showDialog({
|
||||
id: 'workspace-settings',
|
||||
title: 'Workspace Settings',
|
||||
size: 'lg',
|
||||
className: 'h-[50rem]',
|
||||
noPadding: true,
|
||||
render({ hide }) {
|
||||
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { workspacesAtom } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { fuzzyFilter } from 'fuzzbunny';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { KeyboardEvent, ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createFolder } from '../commands/commands';
|
||||
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { switchWorkspace } from '../commands/switchWorkspace';
|
||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
@@ -12,7 +13,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import { useAllRequests } from '../hooks/useAllRequests';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDebouncedState } from '../hooks/useDebouncedState';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
@@ -72,7 +72,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
const activeCookieJar = useActiveCookieJar();
|
||||
const [recentRequests] = useRecentRequests();
|
||||
const [, setSidebarHidden] = useSidebarHidden();
|
||||
const { mutate: createEnvironment } = useCreateEnvironment();
|
||||
const { mutate: sendRequest } = useSendAnyHttpRequest();
|
||||
|
||||
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
|
||||
@@ -139,7 +138,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
{
|
||||
key: 'environment.create',
|
||||
label: 'Create Environment',
|
||||
onSelect: () => createEnvironment(baseEnvironment),
|
||||
onSelect: () => createEnvironmentAndActivate.mutate(baseEnvironment),
|
||||
},
|
||||
{
|
||||
key: 'sidebar.toggle',
|
||||
@@ -190,7 +189,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
activeEnvironment,
|
||||
activeRequest,
|
||||
baseEnvironment,
|
||||
createEnvironment,
|
||||
createWorkspace,
|
||||
httpRequestActions,
|
||||
sendRequest,
|
||||
@@ -350,10 +348,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
|
||||
const filteredGroups = groups
|
||||
.map((g) => {
|
||||
g.items = result
|
||||
const items = result
|
||||
.filter((i) => g.items.find((i2) => i2.key === i.key))
|
||||
.slice(0, MAX_PER_GROUP);
|
||||
return g;
|
||||
return { ...g, items };
|
||||
})
|
||||
.filter((g) => g.items.length > 0);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useTimedBoolean } from '../hooks/useTimedBoolean';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { showToast } from '../lib/toast';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -9,7 +9,6 @@ interface Props extends Omit<ButtonProps, 'onClick'> {
|
||||
}
|
||||
|
||||
export function CopyButton({ text, ...props }: Props) {
|
||||
const copy = useCopy({ disableToast: true });
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<Button
|
||||
@@ -23,8 +22,8 @@ export function CopyButton({ text, ...props }: Props) {
|
||||
message: 'Failed to copy',
|
||||
});
|
||||
} else {
|
||||
copy(content);
|
||||
setCopied();
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useTimedBoolean } from '../hooks/useTimedBoolean';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { showToast } from '../lib/toast';
|
||||
import type { IconButtonProps } from './core/IconButton';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -9,7 +9,6 @@ interface Props extends Omit<IconButtonProps, 'onClick' | 'icon'> {
|
||||
}
|
||||
|
||||
export function CopyIconButton({ text, ...props }: Props) {
|
||||
const copy = useCopy({ disableToast: true });
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<IconButton
|
||||
@@ -25,7 +24,7 @@ export function CopyIconButton({ text, ...props }: Props) {
|
||||
message: 'Failed to copy',
|
||||
});
|
||||
} else {
|
||||
copy(content);
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
||||
as="form"
|
||||
space={3}
|
||||
alignItems="start"
|
||||
className="pb-3 max-h-[50vh]"
|
||||
className="pb-3"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const workspaceId = await createGlobalModel({ model: 'workspace', name });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user