Compare commits

..

59 Commits

Author SHA1 Message Date
Gregory Schier
6f50f35519 Bump Tauri to fix macOS 13 launch issue 2024-10-15 09:54:21 -07:00
Gregory Schier
4e775b2b49 Undo minimumSystemVersion 2024-10-15 07:49:27 -07:00
Gregory Schier
e77a9e5d44 Rebuild plugins 2024-10-15 07:48:26 -07:00
Gregory Schier
a381e44d8c Prevent stale content flash after editing request name 2024-10-15 07:32:00 -07:00
Gregory Schier
4acf0969e8 Only sync models from active workspace 2024-10-15 07:31:42 -07:00
Gregory Schier
30c4178269 Disable autocomplete/correct/etc in plain input 2024-10-14 21:46:48 -07:00
Gregory Schier
dffe6e0a16 Intelligent readonly editor updates, to preserve scroll 2024-10-14 10:40:09 -07:00
Gregory Schier
8090e67b9e Revert hyper v1 for gRPC 2024-10-12 22:05:17 -07:00
Gregory Schier
f1beabcb6f Try again 2024-10-12 21:33:45 -07:00
Gregory Schier
647b8e2313 Try fix windows build 2024-10-12 21:17:44 -07:00
Gregory Schier
f5b4697608 Npm i 2024-10-12 21:06:19 -07:00
Gregory Schier
f201857d51 Bump Tauri to fix settings window 2024-10-12 20:57:01 -07:00
Gregory Schier
0d982057a5 Add proxy setting for HTTP requests (#127) 2024-10-12 20:55:09 -07:00
Gregory Schier
6fb94384b9 Better fuzzy matching in cmd palette 2024-10-12 07:41:01 -07:00
Gregory Schier
d754e7233d Server sent event response viewer (#126) 2024-10-11 06:52:32 -07:00
Gregory Schier
f974a66086 Fix double-click-maximize and backdrop 2024-10-10 07:11:43 -07:00
Gregory Schier
250625fc0e Always show window controls, and open Linux settings in dialog 2024-10-10 06:22:11 -07:00
Gregory Schier
16e090b520 Fix content type detection 2024-10-09 17:20:09 -07:00
Gregory Schier
be9fbbcb6e Fix content type detection 2024-10-09 17:19:41 -07:00
Gregory Schier
8be3c3d0e1 Disable response copy until response is done 2024-10-09 16:38:12 -07:00
Gregory Schier
c680e15cb5 Max width on request name in header 2024-10-09 16:38:03 -07:00
Gregory Schier
da6baf72f5 Response Streaming (#124) 2024-10-09 16:27:37 -07:00
Gregory Schier
2ca30bcb31 Fix Codemirror undo history 2024-10-09 12:00:52 -07:00
Gregory Schier
2e2b3128c5 Fix cookie jar query 2024-10-09 11:26:19 -07:00
Gregory Schier
4a81818d05 Add descriptions to template functions 2024-10-09 11:25:51 -07:00
Gregory Schier
0eb98a3882 Log query errors 2024-10-09 11:25:05 -07:00
Gregory Schier
d28100d682 Add new plugins 2024-10-09 09:54:15 -07:00
Gregory Schier
0f4d3bdbb5 Allow space in dropdown filter text 2024-10-09 09:54:07 -07:00
Gregory Schier
c7eccddac9 Fix performance related to having 100s of requests (#123) 2024-10-08 14:16:57 -07:00
Gregory Schier
4b7712df80 Better Dropdown size calculation for scrolling when not enough room 2024-10-02 16:17:28 -07:00
Gregory Schier
e5c6c31e02 Fix prompt again 2024-10-02 12:53:58 -07:00
Gregory Schier
7e62bb6b68 Fix prompt 2024-10-02 12:19:43 -07:00
Gregory Schier
3b2ee25d75 Fix tauri dialog API usage 2024-10-02 11:45:42 -07:00
Gregory Schier
4a9e2ac9b6 Specify minimum macOS version 13 in tauri conf 2024-10-02 11:39:48 -07:00
Gregory Schier
d890b8be0a Tauri 2.0.0 stable 2024-10-02 11:30:39 -07:00
Gregory Schier
bb1ba93676 Better KeyValueRow sizing 2024-10-02 11:22:20 -07:00
Gregory Schier
f8c3f71cfe Increase node download timeout 2024-10-02 10:14:51 -07:00
Gregory Schier
c9050bd3bb Add CLI to release.yml 2024-10-02 10:04:21 -07:00
Gregory Schier
d0fe1beee0 Fix lint errors 2024-10-02 10:00:58 -07:00
Gregory Schier
e401e8f1cf Fix response header table sizing 2024-10-02 08:22:38 -07:00
Gregory Schier
4160e5b1c4 Make prompt() to return null on cancel 2024-10-02 05:54:44 -07:00
Gregory Schier
89ff25cd54 Fix variables in bulk editor 2024-10-02 05:10:34 -07:00
Gregory Schier
7a941016a9 Fix plugin types 2024-10-01 11:02:38 -07:00
Gregory Schier
7e4f807f75 Add prompt() plugin API (#121) 2024-10-01 08:32:42 -07:00
Gregory Schier
be60e4648a Apply Request path parameters during render (#120) 2024-10-01 08:26:59 -07:00
Gregory Schier
6060ddcd87 Add aliases field to template functions 2024-09-30 18:28:52 -07:00
Gregory Schier
9915c57817 Plugin execution context (#119) 2024-09-30 17:45:51 -07:00
Gregory Schier
917adcfb2e Better plugin development experience (#118) 2024-09-29 10:41:07 -07:00
Gregory Schier
1c5e62a468 Set max-width on toast 2024-09-29 07:20:56 -07:00
Gregory Schier
f5e8c525e9 Add brackets to URL regex 2024-09-28 14:31:18 -07:00
Gregory Schier
6583615885 Update package.json 2024-09-27 06:01:31 -07:00
Gregory Schier
0f0eba244c Update config.yml 2024-09-27 05:59:23 -07:00
Gregory Schier
3911b7b583 Update README.md 2024-09-27 05:58:29 -07:00
Gregory Schier
3634d315ed Update config.yml 2024-09-27 05:49:58 -07:00
Gregory Schier
89b85c2e27 Update issue description 2024-09-27 05:49:20 -07:00
Gregory Schier
bbff3f5969 Remove bug report template 2024-09-27 05:47:24 -07:00
Gregory Schier
eb8f66dca2 Add npm start command back 2024-09-27 05:46:12 -07:00
Gregory Schier
08531fa1ff Update development doc 2024-09-27 05:45:48 -07:00
i-usebruno
662bc41cb3 Improve dev docs (#117)
Co-authored-by: i-usebruno <anon.repressed498@passinbox.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2024-09-27 05:34:15 -07:00
186 changed files with 181633 additions and 11519 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
src-tauri/vendored/**/* linguist-generated=true
src-tauri/gen/schemas/**/* linguist-generated=true

View File

@@ -1,45 +0,0 @@
name: Bug Report
description: "Something isn't working properly in Yaak"
title: "Short description"
labels: ["bug", "needs triage"]
assignees:
- gschier
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report 🤗
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
placeholder: 2024.8.0
validations:
required: true
- type: dropdown
id: os
attributes:
label: What operating system are you on?
multiple: false
options:
- macOS
- Windows
- Linux
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request, Question, etc.
- name: Bugs, Feedback, Feature Requests, and Questions
url: https://feedback.yaak.app
about: Report all non-bugs to the feedback board 👉🏼
about: "Please report to Yaak's public feedback board. Issues will be created and linked here when applicable."

View File

@@ -69,6 +69,7 @@ jobs:
- name: Install NPM Dependencies
run: |
npm ci
npm install @yaakapp/cli
- name: Install Protoc for plugin-runtime
uses: arduino/setup-protoc@v3

View File

@@ -1,19 +1,62 @@
## Developer Setup
# Developer Setup
Development requires the following tools
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
uses Rust and HTML/CSS/JS for the main application but there is also a plugin system powered
by a Node.js sidecar that communicates to the app over gRPC.
Because of the moving parts, there are a few setup steps required before development can
begin.
## Prerequisites
Make sure you have the following tools installed:
- [Node.js](https://nodejs.org/en/download/package-manager)
- [Rust](https://www.rust-lang.org/tools/install)
Then, you can run the app.
Check the installations with the following commands:
1. Checkout the [plugins](https://github.com/yaakapp/plugins) repository
2. Run `YAAK_PLUGINS_DIR="..." npm run bootstrap` to fetch external binaries, build local dependencies, etc.
3. Run the desktop app in dev mode `npm start`
```shell
node -v
npm -v
rustc --version
```
Install the NPM dependencies:
```shell
npm install
```
Run the `bootstrap` command to do some initial setup:
```shell
npm run bootstrap
```
_NOTE: Run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>` to re-build bundled plugins_
## Run the App
After bootstrapping, start the app in development mode:
```shell
npm start
```
_NOTE: If working on bundled plugins, run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>`_
## SQLite Migrations
1. From `src-tauri/`, run `sqlx migrate add migration-name`
2. Migrate the DB by running the app (may need to `cargo clean` first)
New migrations can be created from the `src-tauri/` directory:
```shell
cd src-tauri
sqlx migrate add migration-name
```
_Note: Yaak development builds use a separate database location than production releases_
Run the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._

View File

@@ -7,10 +7,14 @@ requests. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
## Feedback and Bug Reports
Please [Create an Issue](https://github.com/yaakapp/app/issues/new) for bug reports and
submit all other feedback to the [Feedback Board](https://feedback.yaak.app).
All feedback, bug reports, questions, and feature requests should be reported to
[feedback.yaak.app](https://feedback.yaak.app). Issues will be duplicated
in this repository if applicable.
## Contribution Policy
Yaak open source, but currently only accepting contributions for bug fixes. See [
`DEVELOPMENT.md`](DEVELOPMENT.md).
Yaak open source, but only accepting contributions for bug fixes. See the
[`good first issue`](https://github.com/yaakapp/app/labels/good%20first%20issue) label for
issues that are more approachable for contribution.
To get started, visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.

2616
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,14 @@
"src-tauri/yaak_plugin_runtime",
"src-tauri/yaak_sync",
"src-tauri/yaak_templates",
"src-tauri/yaak_sse",
"src-web"
],
"scripts": {
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"bootstrap": "run-p bootstrap:* && npm run --workspace plugin-runtime build",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
"bootstrap:vendor-protoc": "node scripts/vendor-protoc.cjs",
@@ -29,7 +31,7 @@
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0-rc.16",
"@tauri-apps/cli": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"eslint": "^8",

View File

@@ -1,15 +1,18 @@
{
"name": "@yaakapp/api",
"version": "0.2.7",
"version": "0.2.16",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [
"lib/**/*"
],
"scripts": {
"bootstrap": "npm run build",
"build": "run-s build:copy-types build:tsc",
"build:tsc": "tsc",
"build:copy-types": "cpy --flat ../src-tauri/yaak_plugin_runtime/bindings/*.ts ./src/bindings/",
"build:copy-types": "run-p build:copy-types:*",
"build:copy-types:root": "cpy --flat ../src-tauri/yaak_plugin_runtime/bindings/*.ts ./src/bindings",
"build:copy-types:next": "cpy --flat ../src-tauri/yaak_plugin_runtime/bindings/serde_json/*.ts ./src/bindings/serde_json",
"prepublishOnly": "npm run build"
},
"dependencies": {

View File

@@ -4,9 +4,10 @@ import type { Folder } from "./models";
import type { GrpcRequest } from "./models";
import type { HttpRequest } from "./models";
import type { HttpResponse } from "./models";
import type { JsonValue } from "./serde_json/JsonValue";
import type { Workspace } from "./models";
export type BootRequest = { dir: string, };
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
@@ -32,7 +33,7 @@ export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit: number | null, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
@@ -46,9 +47,9 @@ export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpRequestAction = { key: string, label: string, icon: string | null, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle";
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
@@ -56,9 +57,31 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, windowContext: WindowContext, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "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" } & GetHttpRequestActionsRequest | { "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": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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" };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "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" } & GetHttpRequestActionsRequest | { "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": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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" };
export type OpenFileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
require?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
@@ -70,20 +93,145 @@ export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color | null, icon?: Icon | null, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, args: Array<TemplateFunctionArg>, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg | { "type": "file" } & TemplateFunctionFileArg;
export type TemplateFunctionBaseArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionBaseArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionCheckboxArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionCheckboxArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionHttpRequestArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionFileArg = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<OpenFileFilter>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectArg = { options: Array<TemplateFunctionSelectOption>, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionHttpRequestArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectOption = { name: string, value: string, };
export type TemplateFunctionSelectArg = {
/**
* The options that will be available in the select input
*/
options: Array<TemplateFunctionSelectOption>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionTextArg = { placeholder?: string | null, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionSelectOption = { label: string, value: string, };
export type TemplateFunctionTextArg = {
/**
* Placeholder for the text input
*/
placeholder?: string,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -14,10 +14,12 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, url: string, version: string | null, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, variables: Array<EnvironmentVariable>, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };

View File

@@ -3,11 +3,15 @@ import {
FindHttpResponsesResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
PromptTextRequest,
PromptTextResponse,
RenderHttpRequestRequest,
RenderHttpRequestResponse,
SendHttpRequestRequest,
SendHttpRequestResponse,
ShowToastRequest,
TemplateRenderRequest,
TemplateRenderResponse,
} from '..';
export type Context = {
@@ -17,6 +21,9 @@ export type Context = {
toast: {
show(args: ShowToastRequest): void;
};
prompt: {
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
@@ -25,4 +32,7 @@ export type Context = {
httpResponse: {
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
};
templates: {
render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{
"name": "@yaakapp-internal/plugin-runtime",
"scripts": {
"bootstrap": "npm run build",
"build": "run-p build:*",
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../src-tauri/vendored/plugin-runtime/index.cjs",
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=../src-tauri/vendored/plugin-runtime/index.worker.cjs",

View File

@@ -1,14 +1,15 @@
import { InternalEvent } from '@yaakapp/api';
import { BootRequest, InternalEvent } from '@yaakapp-internal/plugin';
import path from 'node:path';
import { Worker } from 'node:worker_threads';
import { EventChannel } from './EventChannel';
import { PluginWorkerData } from './index.worker';
export class PluginHandle {
#worker: Worker;
constructor(
readonly pluginDir: string,
readonly pluginRefId: string,
readonly bootRequest: BootRequest,
readonly events: EventChannel,
) {
this.#worker = this.#createWorker();
@@ -24,28 +25,32 @@ export class PluginHandle {
#createWorker(): Worker {
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
const workerData: PluginWorkerData = {
pluginRefId: this.pluginRefId,
bootRequest: this.bootRequest,
};
const worker = new Worker(workerPath, {
workerData: { pluginDir: this.pluginDir, pluginRefId: this.pluginRefId },
workerData,
});
worker.on('message', (e) => this.events.emit(e));
worker.on('error', this.#handleError.bind(this));
worker.on('exit', this.#handleExit.bind(this));
console.log('Created plugin worker for ', this.pluginDir);
console.log('Created plugin worker for ', this.bootRequest.dir);
return worker;
}
async #handleError(err: Error) {
console.error('Plugin errored', this.pluginDir, err);
console.error('Plugin errored', this.bootRequest.dir, err);
}
async #handleExit(code: number) {
if (code === 0) {
console.log('Plugin exited successfully', this.pluginDir);
console.log('Plugin exited successfully', this.bootRequest.dir);
} else {
console.log('Plugin exited with status', code, this.pluginDir);
console.log('Plugin exited with status', code, this.bootRequest.dir);
}
}
}

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.2.0
// protoc-gen-ts_proto v2.2.3
// protoc v3.19.1
// source: plugins/runtime.proto
@@ -33,13 +33,14 @@ export const EventStreamEvent: MessageFns<EventStreamEvent> = {
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
case 1: {
if (tag !== 10) {
break;
}
message.event = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;

View File

@@ -18,7 +18,7 @@ const plugins: Record<string, PluginHandle> = {};
const pluginEvent: InternalEvent = JSON.parse(e.event);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.payload.dir, pluginEvent.pluginRefId, events);
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, events);
plugins[pluginEvent.pluginRefId] = plugin;
}

View File

@@ -1,26 +1,38 @@
import {
Context,
BootRequest,
FindHttpResponsesResponse,
GetHttpRequestByIdResponse,
HttpRequestAction,
ImportResponse,
InternalEvent,
InternalEventPayload,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
} from '@yaakapp/api';
TemplateRenderResponse,
WindowContext,
} from '@yaakapp-internal/plugin';
import { Context } from '@yaakapp/api';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpRequestActionPlugin';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { Stats, readFileSync, statSync, watch } from 'node:fs';
import { readFileSync, Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
import { parentPort, workerData } from 'node:worker_threads';
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
}
async function initialize() {
const { pluginDir, pluginRefId } = workerData;
const {
bootRequest: { dir: pluginDir, watch: enableWatch },
pluginRefId,
}: PluginWorkerData = workerData;
const pathPkg = path.join(pluginDir, 'package.json');
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
@@ -42,21 +54,26 @@ async function initialize() {
if (typeof mod.pluginHookImport === 'function') capabilities.push('import');
if (typeof mod.pluginHookResponseFilter === 'function') capabilities.push('filter');
console.log('Plugin initialized', pkg.name, capabilities, Object.keys(mod));
console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
function buildEventToSend(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return { pluginRefId, id: genId(), replyId, payload };
return { pluginRefId, id: genId(), replyId, payload, windowContext };
}
function sendEmpty(replyId: string | null = null): string {
return sendPayload({ type: 'empty_response' }, replyId);
function sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
return sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
function sendPayload(payload: InternalEventPayload, replyId: string | null): string {
const event = buildEventToSend(payload, replyId);
function sendPayload(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = buildEventToSend(windowContext, payload, replyId);
sendEvent(event);
return event.id;
}
@@ -69,10 +86,11 @@ async function initialize() {
}
async function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: WindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = buildEventToSend(payload, null);
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>(async (resolve) => {
@@ -97,53 +115,93 @@ async function initialize() {
}
// Reload plugin if JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const cb = async () => {
await reloadModule();
return sendPayload({ type: 'reload_response' }, null);
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
watchFile(pathMod, cb);
watchFile(pathPkg, cb);
if (enableWatch) {
watchFile(pathMod, cb);
watchFile(pathPkg, cb);
}
const ctx: Context = {
const newCtx = (event: InternalEvent): Context => ({
clipboard: {
async copyText(text) {
await sendAndWaitForReply({ type: 'copy_text_request', text });
await sendAndWaitForReply(event.windowContext, { type: 'copy_text_request', text });
},
},
toast: {
async show(args) {
await sendAndWaitForReply({ type: 'show_toast_request', ...args });
await sendAndWaitForReply(event.windowContext, { type: 'show_toast_request', ...args });
},
},
prompt: {
async text(args) {
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
type: 'prompt_text_request',
...args,
});
return reply.value;
},
},
httpResponse: {
async find(args) {
const payload = { type: 'find_http_responses_request', ...args } as const;
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(payload);
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(
event.windowContext,
payload,
);
return httpResponses;
},
},
httpRequest: {
async getById(args) {
const payload = { type: 'get_http_request_by_id_request', ...args } as const;
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(payload);
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
async send(args) {
const payload = { type: 'send_http_request_request', ...args } as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
payload,
);
return httpResponse;
},
async render(args) {
const payload = { type: 'render_http_request_request', ...args } as const;
const result = await sendAndWaitForReply<RenderHttpRequestResponse>(payload);
return result.httpRequest;
const { httpRequest } = await sendAndWaitForReply<RenderHttpRequestResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
},
};
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type
* (eg. object), it will be recursively rendered.
* */
async render(args) {
const payload = { type: 'template_render_request', ...args } as const;
const result = await sendAndWaitForReply<TemplateRenderResponse>(
event.windowContext,
payload,
);
return result.data;
},
},
});
// Message comes into the plugin to be processed
parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => {
parentPort!.on('message', async (event: InternalEvent) => {
let { windowContext, payload, id: replyId } = event;
const ctx = newCtx(event);
try {
if (payload.type === 'boot_request') {
const payload: InternalEventPayload = {
@@ -152,7 +210,7 @@ async function initialize() {
version: pkg.version,
capabilities,
};
sendPayload(payload, replyId);
sendPayload(windowContext, payload, replyId);
return;
}
@@ -160,7 +218,7 @@ async function initialize() {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
sendPayload(payload, replyId);
sendPayload(windowContext, payload, replyId);
return;
}
@@ -171,7 +229,7 @@ async function initialize() {
type: 'import_response',
resources: reply?.resources,
};
sendPayload(replyPayload, replyId);
sendPayload(windowContext, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
@@ -187,7 +245,7 @@ async function initialize() {
type: 'export_http_request_response',
content: reply,
};
sendPayload(replyPayload, replyId);
sendPayload(windowContext, replyPayload, replyId);
return;
}
@@ -200,7 +258,7 @@ async function initialize() {
type: 'filter_response',
content: reply,
};
sendPayload(replyPayload, replyId);
sendPayload(windowContext, replyPayload, replyId);
return;
}
@@ -220,7 +278,7 @@ async function initialize() {
pluginRefId,
actions: reply,
};
sendPayload(replyPayload, replyId);
sendPayload(windowContext, replyPayload, replyId);
return;
}
@@ -240,7 +298,7 @@ async function initialize() {
pluginRefId,
functions: reply,
};
sendPayload(replyPayload, replyId);
sendPayload(windowContext, replyPayload, replyId);
return;
}
@@ -253,7 +311,7 @@ async function initialize() {
);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(replyId);
sendEmpty(windowContext, replyId);
return;
}
}
@@ -267,7 +325,14 @@ async function initialize() {
);
if (typeof action?.onRender === 'function') {
const result = await action.onRender(ctx, payload.args);
sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId);
sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
return;
}
}
@@ -281,7 +346,7 @@ async function initialize() {
}
// No matches, so send back an empty response so the caller doesn't block forever
sendEmpty(replyId);
sendEmpty(windowContext, replyId);
});
}

View File

@@ -1 +1,7 @@
edition = "2018"
# Widths
chain_width = 100
max_width = 100
single_line_if_else_max_width = 100
fn_call_width = 100

View File

@@ -4,7 +4,7 @@ const Downloader = require('nodejs-file-downloader');
const { rmSync, cpSync, mkdirSync, existsSync } = require('node:fs');
const { execSync } = require('node:child_process');
const NODE_VERSION = 'v22.5.1';
const NODE_VERSION = 'v22.9.0';
// `${process.platform}_${process.arch}`
const MAC_ARM = 'darwin_arm64';
@@ -53,7 +53,12 @@ rmSync(tmpDir, { recursive: true, force: true });
(async function () {
// Download GitHub release artifact
const { filePath } = await new Downloader({ url, directory: tmpDir }).download();
console.log('Downloading NodeJS at', url);
const { filePath } = await new Downloader({
url,
directory: tmpDir,
timeout: 1000 * 60 * 2,
}).download();
// Decompress to the same directory
await decompress(filePath, tmpDir, {});

View File

@@ -3,8 +3,8 @@ const path = require('node:path');
const { execSync } = require('node:child_process');
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
if (!pluginsDir) {
console.log('YAAK_PLUGINS_DIR is not set');
process.exit(1);
console.log('Skipping bundled plugins build because YAAK_PLUGINS_DIR is not set');
return;
}
console.log('Installing Yaak plugins dependencies', pluginsDir);

View File

@@ -2,4 +2,5 @@
# will have compiled files and executables
target/
vendored
vendored/*
!vendored/plugins

907
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"]
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models", "yaak_sse"]
[package]
name = "yaak-app"
@@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "lib"]
strip = true # Automatically strip symbols from the binary.
[build-dependencies]
tauri-build = { version = "2.0.0-rc.12", features = [] }
tauri-build = { version = "2.0.1", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
@@ -30,6 +30,7 @@ yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
yaak_plugin_runtime = { workspace = true }
yaak_models = { workspace = true }
yaak_sse = { path = "yaak_sse" }
anyhow = "1.0.86"
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
@@ -46,22 +47,23 @@ serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-dialog = "2.0.0-rc.7"
tauri-plugin-fs = "2.0.0-rc.5"
tauri-plugin-log = { version = "2.0.0-rc.2", features = ["colored"] }
tauri-plugin-os = "2.0.0-rc.1"
tauri-plugin-updater = "2.0.0-rc.3"
tauri-plugin-window-state = "2.0.0-rc.5"
tauri-plugin-clipboard-manager = "2.0.1"
tauri-plugin-dialog = "2.0.1"
tauri-plugin-fs = "2.0.1"
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
tauri-plugin-os = "2.0.1"
tauri-plugin-updater = "2.0.2"
tauri-plugin-window-state = "2.0.1"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
uuid = "1.7.0"
thiserror = "1.0.61"
mime_guess = "2.0.5"
urlencoding = "2.1.3"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.13.0" }
[workspace.dependencies]
yaak_models = { path = "yaak_models" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
tauri-plugin-shell = "2.0.0-rc.3"
tauri = { version = "2.0.0-rc.15", features = ["devtools", "protocol-asset"] }
tauri-plugin-shell = "2.0.1"
tauri = { version = "2.0.4", features = ["devtools", "protocol-asset"] }

View File

@@ -32,6 +32,7 @@
"shell:allow-open",
"core:webview:allow-set-webview-zoom",
"core:window:allow-close",
"core:window:allow-internal-toggle-maximize",
"core:window:allow-is-fullscreen",
"core:window:allow-maximize",
"core:window:allow-minimize",

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}

View File

@@ -1704,72 +1704,122 @@
"properties": {
"allow": {
"items": {
"title": "Entry",
"description": "A command allowed to be executed by the webview API.",
"type": "object",
"required": [
"args",
"cmd",
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellAllowedArgs"
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
]
},
"additionalProperties": false
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
}
]
}
},
"deny": {
"items": {
"title": "Entry",
"description": "A command allowed to be executed by the webview API.",
"type": "object",
"required": [
"args",
"cmd",
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellAllowedArgs"
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
]
},
"additionalProperties": false
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
}
]
}
}
}
@@ -1922,6 +1972,11 @@
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-set-app-theme"
},
{
"description": "Enables the tauri_version command without any pre-configured scope.",
"type": "string",
@@ -1952,6 +2007,11 @@
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-set-app-theme"
},
{
"description": "Denies the tauri_version command without any pre-configured scope.",
"type": "string",
@@ -2507,6 +2567,11 @@
"type": "string",
"const": "core:webview:default"
},
{
"description": "Enables the clear_all_browsing_data command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-clear-all-browsing-data"
},
{
"description": "Enables the create_webview command without any pre-configured scope.",
"type": "string",
@@ -2562,16 +2627,31 @@
"type": "string",
"const": "core:webview:allow-webview-close"
},
{
"description": "Enables the webview_hide command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-hide"
},
{
"description": "Enables the webview_position command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-position"
},
{
"description": "Enables the webview_show command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-show"
},
{
"description": "Enables the webview_size command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-size"
},
{
"description": "Denies the clear_all_browsing_data command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-clear-all-browsing-data"
},
{
"description": "Denies the create_webview command without any pre-configured scope.",
"type": "string",
@@ -2627,11 +2707,21 @@
"type": "string",
"const": "core:webview:deny-webview-close"
},
{
"description": "Denies the webview_hide command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-hide"
},
{
"description": "Denies the webview_position command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-position"
},
{
"description": "Denies the webview_show command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-show"
},
{
"description": "Denies the webview_size command without any pre-configured scope.",
"type": "string",
@@ -2712,6 +2802,11 @@
"type": "string",
"const": "core:window:allow-is-decorated"
},
{
"description": "Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-enabled"
},
{
"description": "Enables the is_focused command without any pre-configured scope.",
"type": "string",
@@ -2842,6 +2937,11 @@
"type": "string",
"const": "core:window:allow-set-effects"
},
{
"description": "Enables the set_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-set-enabled"
},
{
"description": "Enables the set_focus command without any pre-configured scope.",
"type": "string",
@@ -2917,6 +3017,11 @@
"type": "string",
"const": "core:window:allow-set-skip-taskbar"
},
{
"description": "Enables the set_theme command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-set-theme"
},
{
"description": "Enables the set_title command without any pre-configured scope.",
"type": "string",
@@ -3042,6 +3147,11 @@
"type": "string",
"const": "core:window:deny-is-decorated"
},
{
"description": "Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-enabled"
},
{
"description": "Denies the is_focused command without any pre-configured scope.",
"type": "string",
@@ -3172,6 +3282,11 @@
"type": "string",
"const": "core:window:deny-set-effects"
},
{
"description": "Denies the set_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-set-enabled"
},
{
"description": "Denies the set_focus command without any pre-configured scope.",
"type": "string",
@@ -3247,6 +3362,11 @@
"type": "string",
"const": "core:window:deny-set-skip-taskbar"
},
{
"description": "Denies the set_theme command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-set-theme"
},
{
"description": "Denies the set_title command without any pre-configured scope.",
"type": "string",
@@ -5118,7 +5238,7 @@
}
]
},
"ShellAllowedArg": {
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
@@ -5146,18 +5266,18 @@
}
]
},
"ShellAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellAllowedArg"
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1704,72 +1704,122 @@
"properties": {
"allow": {
"items": {
"title": "Entry",
"description": "A command allowed to be executed by the webview API.",
"type": "object",
"required": [
"args",
"cmd",
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellAllowedArgs"
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
]
},
"additionalProperties": false
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
}
]
}
},
"deny": {
"items": {
"title": "Entry",
"description": "A command allowed to be executed by the webview API.",
"type": "object",
"required": [
"args",
"cmd",
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellAllowedArgs"
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
]
},
"additionalProperties": false
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
}
]
}
}
}
@@ -1922,6 +1972,11 @@
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-set-app-theme"
},
{
"description": "Enables the tauri_version command without any pre-configured scope.",
"type": "string",
@@ -1952,6 +2007,11 @@
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-set-app-theme"
},
{
"description": "Denies the tauri_version command without any pre-configured scope.",
"type": "string",
@@ -2507,6 +2567,11 @@
"type": "string",
"const": "core:webview:default"
},
{
"description": "Enables the clear_all_browsing_data command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-clear-all-browsing-data"
},
{
"description": "Enables the create_webview command without any pre-configured scope.",
"type": "string",
@@ -2562,16 +2627,31 @@
"type": "string",
"const": "core:webview:allow-webview-close"
},
{
"description": "Enables the webview_hide command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-hide"
},
{
"description": "Enables the webview_position command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-position"
},
{
"description": "Enables the webview_show command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-show"
},
{
"description": "Enables the webview_size command without any pre-configured scope.",
"type": "string",
"const": "core:webview:allow-webview-size"
},
{
"description": "Denies the clear_all_browsing_data command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-clear-all-browsing-data"
},
{
"description": "Denies the create_webview command without any pre-configured scope.",
"type": "string",
@@ -2627,11 +2707,21 @@
"type": "string",
"const": "core:webview:deny-webview-close"
},
{
"description": "Denies the webview_hide command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-hide"
},
{
"description": "Denies the webview_position command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-position"
},
{
"description": "Denies the webview_show command without any pre-configured scope.",
"type": "string",
"const": "core:webview:deny-webview-show"
},
{
"description": "Denies the webview_size command without any pre-configured scope.",
"type": "string",
@@ -2712,6 +2802,11 @@
"type": "string",
"const": "core:window:allow-is-decorated"
},
{
"description": "Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-enabled"
},
{
"description": "Enables the is_focused command without any pre-configured scope.",
"type": "string",
@@ -2842,6 +2937,11 @@
"type": "string",
"const": "core:window:allow-set-effects"
},
{
"description": "Enables the set_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-set-enabled"
},
{
"description": "Enables the set_focus command without any pre-configured scope.",
"type": "string",
@@ -2917,6 +3017,11 @@
"type": "string",
"const": "core:window:allow-set-skip-taskbar"
},
{
"description": "Enables the set_theme command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-set-theme"
},
{
"description": "Enables the set_title command without any pre-configured scope.",
"type": "string",
@@ -3042,6 +3147,11 @@
"type": "string",
"const": "core:window:deny-is-decorated"
},
{
"description": "Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-enabled"
},
{
"description": "Denies the is_focused command without any pre-configured scope.",
"type": "string",
@@ -3172,6 +3282,11 @@
"type": "string",
"const": "core:window:deny-set-effects"
},
{
"description": "Denies the set_enabled command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-set-enabled"
},
{
"description": "Denies the set_focus command without any pre-configured scope.",
"type": "string",
@@ -3247,6 +3362,11 @@
"type": "string",
"const": "core:window:deny-set-skip-taskbar"
},
{
"description": "Denies the set_theme command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-set-theme"
},
{
"description": "Denies the set_title command without any pre-configured scope.",
"type": "string",
@@ -5118,7 +5238,7 @@
}
]
},
"ShellAllowedArg": {
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
@@ -5146,18 +5266,18 @@
}
]
},
"ShellAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellAllowedArg"
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]

View File

@@ -0,0 +1,5 @@
ALTER TABLE http_responses
ADD COLUMN state TEXT DEFAULT 'closed' NOT NULL;
ALTER TABLE grpc_connections
ADD COLUMN state TEXT DEFAULT 'closed' NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN proxy TEXT;

View File

@@ -1,7 +1,4 @@
use std::collections::BTreeMap;
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
@@ -14,33 +11,48 @@ use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{error, warn};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::Method;
use reqwest::{multipart, Url};
use reqwest::{multipart, Proxy, Url};
use reqwest::{Method, Response};
use serde_json::Value;
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot;
use tokio::fs;
use tokio::fs::{create_dir_all, File};
use tokio::io::AsyncWriteExt;
use tokio::sync::watch::Receiver;
use tokio::sync::{oneshot, Mutex};
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, HttpUrlParameter,
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
HttpResponseState, ProxySetting, ProxySettingAuth,
};
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
use yaak_models::queries::{
get_http_response, get_or_create_settings, get_workspace, update_response_if_id,
upsert_cookie_jar,
};
use yaak_plugin_runtime::events::{RenderPurpose, WindowContext};
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &HttpRequest,
response: &HttpResponse,
og_response: &HttpResponse,
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancel_rx: &mut Receiver<bool>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let workspace = get_workspace(window, &request.workspace_id)
.await
.expect("Failed to get Workspace");
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
let cb = cb.for_send();
let workspace =
get_workspace(window, &request.workspace_id).await.expect("Failed to get Workspace");
let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(window),
RenderPurpose::Send,
);
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let rendered_request =
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
@@ -50,6 +62,7 @@ pub async fn send_http_request<R: Runtime>(
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
debug!("Sending request to {url_string}");
let mut client_builder = reqwest::Client::builder()
.redirect(match workspace.setting_follow_redirects {
@@ -64,6 +77,31 @@ pub async fn send_http_request<R: Runtime>(
.danger_accept_invalid_certs(!workspace.setting_validate_certificates)
.tls_info(true);
match settings.proxy {
Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(),
Some(ProxySetting::Enabled { http, https, auth }) => {
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()) };
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
let proxy_url = match (url.scheme(), http, https) {
("http", Some(proxy_url), _) => Some(proxy_url),
("https", _, Some(proxy_url)) => Some(proxy_url),
_ => None,
};
proxy_url
});
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
client_builder = client_builder.proxy(proxy);
}
None => {} // 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) => {
@@ -103,38 +141,30 @@ pub async fn send_http_request<R: Runtime>(
if !p.enabled || p.name.is_empty() {
continue;
}
// Replace path parameters with values from URL parameters
let old_url_string = url_string.clone();
url_string = replace_path_placeholder(&p, url_string.as_str());
// Treat as regular param if wasn't used as path param
if old_url_string == url_string {
query_params.push((p.name, p.value));
}
query_params.push((p.name, p.value));
}
let uri = match http::Uri::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
return response_err(
response,
return Ok(response_err(
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await;
.await);
}
};
// 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()) {
Ok(u) => u,
Err(e) => {
return response_err(
response,
return Ok(response_err(
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await;
.await);
}
};
@@ -193,16 +223,8 @@ pub async fn send_http_request<R: Runtime>(
let a = rendered_request.authentication;
if b == "basic" {
let username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
let password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
let username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or_default();
let password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or_default();
let auth = format!("{username}:{password}");
let encoded = BASE64_STANDARD.encode(auth);
@@ -211,11 +233,7 @@ pub async fn send_http_request<R: Runtime>(
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let token = a
.get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or_default();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
@@ -229,10 +247,7 @@ pub async fn send_http_request<R: Runtime>(
let query = get_str_h(&request_body, "query");
let variables = get_str_h(&request_body, "variables");
let body = if variables.trim().is_empty() {
format!(
r#"{{"query":{}}}"#,
serde_json::to_string(query).unwrap_or_default()
)
format!(r#"{{"query":{}}}"#, serde_json::to_string(query).unwrap_or_default())
} else {
format!(
r#"{{"query":{},"variables":{variables}}}"#,
@@ -272,12 +287,12 @@ pub async fn send_http_request<R: Runtime>(
.as_str()
.unwrap_or_default();
match fs::read(file_path).map_err(|e| e.to_string()) {
match fs::read(file_path).await.map_err(|e| e.to_string()) {
Ok(f) => {
request_builder = request_builder.body(f);
}
Err(e) => {
return response_err(response, e, window).await;
return Ok(response_err(&*response.lock().await, e, window).await);
}
}
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
@@ -300,10 +315,15 @@ pub async fn send_http_request<R: Runtime>(
let mut part = if file_path.is_empty() {
multipart::Part::text(value.clone())
} else {
match fs::read(file_path.clone()) {
match fs::read(file_path.clone()).await {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
return Ok(response_err(
&*response.lock().await,
e.to_string(),
window,
)
.await);
}
}
};
@@ -318,9 +338,8 @@ pub async fn send_http_request<R: Runtime>(
Mime::from_str("application/octet-stream").unwrap();
let mime =
mime_guess::from_path(file_path.clone()).first_or(default_mime);
part = part
.mime_str(mime.essence_str())
.map_err(|e| e.to_string())?;
part =
part.mime_str(mime.essence_str()).map_err(|e| e.to_string())?;
}
// Set file path if not empty
@@ -351,118 +370,179 @@ pub async fn send_http_request<R: Runtime>(
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), window).await;
warn!("Failed to build request builder {e:?}");
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
}
};
let start = std::time::Instant::now();
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();
let (resp_tx, resp_rx) = oneshot::channel();
let start = std::time::Instant::now();
tokio::spawn(async move {
let _ = resp_tx.send(client.execute(sendable_req).await);
});
let raw_response = tokio::select! {
Ok(r) = resp_rx => {r}
_ = cancel_rx.changed() => {
return response_err(response, "Request was cancelled".to_string(), window).await;
Ok(r) = resp_rx => r,
_ = cancelled_rx.changed() => {
debug!("Request cancelled");
return Ok(response_err(&*response.lock().await, "Request was cancelled".to_string(), window).await);
}
};
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.elapsed_headers = start.elapsed().as_millis() as i32;
let response_headers = v.headers().clone();
response.status = v.status().as_u16() as i32;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = response_headers
.iter()
.map(|(k, v)| HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap_or_default().to_string(),
})
.collect();
response.url = v.url().to_string();
response.remote_addr = v.remote_addr().map(|a| a.to_string());
response.version = match v.version() {
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
_ => None,
{
let window = window.clone();
let cancelled_rx = cancelled_rx.clone();
let response_id = response_id.clone();
let response = response.clone();
tokio::spawn(async move {
match raw_response {
Ok(mut v) => {
let content_length = v.content_length();
let response_headers = v.headers().clone();
let dir = window.app_handle().path().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).await.expect("Failed to create responses dir");
let body_path = if response_id.is_empty() {
base_dir.join(response_id.clone())
} else {
base_dir.join(uuid::Uuid::new_v4().to_string())
};
{
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.status = v.status().as_u16() as i32;
r.status_reason = v.status().canonical_reason().map(|s| s.to_string());
r.headers = response_headers
.iter()
.map(|(k, v)| HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap_or_default().to_string(),
})
.collect();
r.url = v.url().to_string();
r.remote_addr = v.remote_addr().map(|a| a.to_string());
r.version = match v.version() {
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
_ => None,
};
r.state = HttpResponseState::Connected;
update_response_if_id(&window, &r)
.await
.expect("Failed to update response after connected");
}
// Write body to FS
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.await
.expect("Failed to open file");
let mut written_bytes: usize = 0;
loop {
let chunk = v.chunk().await;
if *cancelled_rx.borrow() {
// Request was canceled
return;
}
match chunk {
Ok(Some(bytes)) => {
f.write_all(&bytes).await.expect("Failed to write to file");
f.flush().await.expect("Failed to flush file");
written_bytes += bytes.len();
let mut r = response.lock().await;
r.elapsed = start.elapsed().as_millis() as i32;
r.content_length = Some(written_bytes as i32);
update_response_if_id(&window, &r)
.await
.expect("Failed to update response");
}
Ok(None) => {
break;
}
Err(e) => {
response_err(&*response.lock().await, e.to_string(), &window).await;
break;
}
}
}
// Set final content length
{
let mut r = response.lock().await;
r.content_length = match content_length {
Some(l) => Some(l as i32),
None => Some(written_bytes as i32),
};
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r)
.await
.expect("Failed to update response");
};
// Add cookie store if specified
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
// println!("RESPONSE COOKIE: {}", h.to_str().unwrap());
// cookie_store::RawCookie::from_str(h.to_str().unwrap())
// .expect("Failed to parse cookie")
// });
// store.store_response_cookies(cookies, &url);
let json_cookies: Vec<Cookie> = cookie_store
.lock()
.unwrap()
.iter_any()
.map(|c| {
let json_cookie =
serde_json::to_value(&c).expect("Failed to serialize cookie");
serde_json::from_value(json_cookie)
.expect("Failed to deserialize cookie")
})
.collect::<Vec<_>>();
cookie_jar.cookies = json_cookies;
if let Err(e) = upsert_cookie_jar(&window, &cookie_jar).await {
error!("Failed to update cookie jar: {}", e);
};
}
}
Err(e) => {
warn!("Failed to execute request {e}");
response_err(&*response.lock().await, format!("{e}{e:?}"), &window).await;
}
};
let content_length = v.content_length();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.elapsed = start.elapsed().as_millis() as i32;
let r = response.lock().await.clone();
done_tx.send(r).unwrap();
});
};
// Use content length if available, otherwise use body length
response.content_length = match content_length {
Some(l) => Some(l as i32),
None => Some(body_bytes.len() as i32),
};
{
// Write body to FS
let dir = window.app_handle().path().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
Ok(tokio::select! {
Ok(r) = done_rx => r,
_ = cancelled_rx.changed() => {
match get_http_response(window, response_id.as_str()).await {
Ok(mut r) => {
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r).await.expect("Failed to update response")
},
_ => {
response_err(&*response.lock().await, "Ephemeral request was cancelled".to_string(), &window).await
}.clone(),
}
response = update_response_if_id(window, &response)
.await
.expect("Failed to update response");
// Add cookie store if specified
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
// println!("RESPONSE COOKIE: {}", h.to_str().unwrap());
// cookie_store::RawCookie::from_str(h.to_str().unwrap())
// .expect("Failed to parse cookie")
// });
// store.store_response_cookies(cookies, &url);
let json_cookies: Vec<Cookie> = cookie_store
.lock()
.unwrap()
.iter_any()
.map(|c| {
let json_cookie =
serde_json::to_value(&c).expect("Failed to serialize cookie");
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.collect::<Vec<_>>();
cookie_jar.cookies = json_cookies;
if let Err(e) = upsert_cookie_jar(window, &cookie_jar).await {
error!("Failed to update cookie jar: {}", e);
};
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), window).await,
}
})
}
fn ensure_proto(url_str: &str) -> String {
@@ -508,123 +588,3 @@ fn get_str_h<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
Some(v) => v.as_str().unwrap_or_default(),
}
}
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
if !p.enabled {
return url.to_string();
}
if !p.name.starts_with(":") {
return url.to_string();
}
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
let result = re
.replace_all(url, |cap: &regex::Captures| {
format!(
"{}{}{}",
cap[1].to_string(),
urlencoding::encode(p.value.as_str()),
cap[2].to_string()
)
})
.into_owned();
result
}
#[cfg(test)]
mod tests {
use crate::http_request::replace_path_placeholder;
use yaak_models::models::HttpUrlParameter;
#[test]
fn placeholder_middle() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
"https://example.com/xxx/bar",
);
}
#[test]
fn placeholder_end() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/xxx",
);
}
#[test]
fn placeholder_query() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
"https://example.com/xxx?:foo",
);
}
#[test]
fn placeholder_missing() {
let p = HttpUrlParameter {
enabled: true,
name: "".to_string(),
value: "".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
"https://example.com/:missing",
);
}
#[test]
fn placeholder_disabled() {
let p = HttpUrlParameter {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/:foo",
);
}
#[test]
fn placeholder_prefix() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
"https://example.com/:foooo",
);
}
#[test]
fn placeholder_encode() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/Hello%20World",
);
}
}

View File

@@ -3,7 +3,7 @@ extern crate core;
extern crate objc;
use std::collections::BTreeMap;
use std::fs::{create_dir_all, read_to_string, File};
use std::fs::{create_dir_all, File};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
@@ -13,9 +13,12 @@ use std::{fs, panic};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use chrono::Utc;
use eventsource_client::{EventParser, SSE};
use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn};
use rand::random;
use regex::Regex;
use serde::Serialize;
use serde_json::{json, Value};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
@@ -25,6 +28,7 @@ use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::fs::read_to_string;
use tokio::sync::Mutex;
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
@@ -36,34 +40,38 @@ use crate::export_resources::{get_workspace_export_resources, WorkspaceExportRes
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_http_request, render_template};
use crate::render::{render_grpc_request, render_http_request, render_json_value, render_template};
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use yaak_models::models::{
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
ModelType, Plugin, Settings, Workspace,
};
use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_plugin, delete_workspace, duplicate_grpc_request,
duplicate_http_request, generate_model_id, get_cookie_jar, get_environment, get_folder,
get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
delete_all_grpc_connections, delete_all_http_responses_for_request, delete_cookie_jar,
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
delete_http_request, delete_http_response, delete_plugin, delete_workspace,
duplicate_grpc_request, duplicate_http_request, generate_id, generate_model_id, get_cookie_jar,
get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request,
get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace,
list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events,
list_grpc_requests, list_http_requests, list_http_responses, list_http_responses_for_request,
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
};
use yaak_plugin_runtime::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon,
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
ShowToastRequest,
InternalEvent, InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse,
RenderPurpose, SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse,
WindowContext,
};
use yaak_plugin_runtime::plugin_handle::PluginHandle;
use yaak_sse::sse::ServerSentEvent;
use yaak_templates::{Parser, Tokens};
mod analytics;
@@ -121,8 +129,9 @@ async fn cmd_template_tokens_to_string(tokens: Tokens) -> Result<String, String>
}
#[tauri::command]
async fn cmd_render_template(
window: WebviewWindow,
async fn cmd_render_template<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
template: &str,
workspace_id: &str,
environment_id: Option<&str>,
@@ -139,18 +148,22 @@ async fn cmd_render_template(
.await
.map_err(|e| e.to_string())?;
let rendered = render_template(
window.app_handle(),
template,
&workspace,
environment.as_ref(),
&PluginTemplateCallback::new(
&app_handle,
&WindowContext::from_window(&window),
RenderPurpose::Preview,
),
)
.await;
Ok(rendered)
}
#[tauri::command]
async fn cmd_dismiss_notification(
window: WebviewWindow,
async fn cmd_dismiss_notification<R: Runtime>(
window: WebviewWindow<R>,
notification_id: &str,
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
) -> Result<(), String> {
@@ -162,10 +175,10 @@ async fn cmd_dismiss_notification(
}
#[tauri::command]
async fn cmd_grpc_reflect(
async fn cmd_grpc_reflect<R: Runtime>(
request_id: &str,
proto_files: Vec<String>,
window: WebviewWindow,
window: WebviewWindow<R>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<Vec<ServiceDefinition>, String> {
let req = get_grpc_request(&window, request_id)
@@ -189,11 +202,11 @@ async fn cmd_grpc_reflect(
}
#[tauri::command]
async fn cmd_grpc_go(
async fn cmd_grpc_go<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
proto_files: Vec<String>,
window: WebviewWindow,
window: WebviewWindow<R>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
let environment = match environment_id {
@@ -210,8 +223,17 @@ async fn cmd_grpc_go(
let workspace = get_workspace(&window, &req.workspace_id)
.await
.map_err(|e| e.to_string())?;
let req =
render_grpc_request(window.app_handle(), &req, &workspace, environment.as_ref()).await;
let req = render_grpc_request(
&req,
&workspace,
environment.as_ref(),
&PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(&window),
RenderPurpose::Send,
),
)
.await;
let mut metadata = BTreeMap::new();
// Add the rest of metadata
@@ -262,6 +284,7 @@ async fn cmd_grpc_go(
request_id: req.id,
status: -1,
elapsed: 0,
state: GrpcConnectionState::Initialized,
url: req.url.clone(),
..Default::default()
},
@@ -317,6 +340,7 @@ async fn cmd_grpc_go(
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i32,
error: Some(err.clone()),
state: GrpcConnectionState::Closed,
..conn.clone()
},
)
@@ -345,7 +369,7 @@ async fn cmd_grpc_go(
move |ev: tauri::Event| {
if *cancelled_rx.borrow() {
// Stream is cancelled
// Stream is canceled
return;
}
@@ -572,6 +596,7 @@ async fn cmd_grpc_go(
stream.into_inner()
}
Some(Err(e)) => {
warn!("GRPC stream error {e:?}");
upsert_grpc_event(
&w,
&(match e.status {
@@ -624,7 +649,7 @@ async fn cmd_grpc_go(
&w,
&GrpcEvent {
content: "Connection complete".to_string(),
status: Some(Code::Unavailable as i32),
status: Some(Code::Ok as i32),
metadata: metadata_to_map(trailers),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
@@ -671,6 +696,7 @@ async fn cmd_grpc_go(
&GrpcConnection{
elapsed: start.elapsed().as_millis() as i32,
status: closed_status,
state: GrpcConnectionState::Closed,
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
},
).await.unwrap();
@@ -690,6 +716,7 @@ async fn cmd_grpc_go(
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i32,
status: Code::Cancelled as i32,
state: GrpcConnectionState::Closed,
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
},
)
@@ -734,7 +761,9 @@ async fn cmd_send_ephemeral_request(
window.listen_any(
format!("cancel_http_response_{}", response.id),
move |_event| {
let _ = cancel_tx.send(true);
if let Err(e) = cancel_tx.send(true) {
warn!("Failed to send cancel event for ephemeral request {e:?}");
}
},
);
@@ -750,13 +779,13 @@ async fn cmd_send_ephemeral_request(
}
#[tauri::command]
async fn cmd_filter_response(
w: WebviewWindow,
async fn cmd_filter_response<R: Runtime>(
window: WebviewWindow<R>,
response_id: &str,
plugin_manager: State<'_, PluginManager>,
filter: &str,
) -> Result<FilterResponse, String> {
let response = get_http_response(&w, response_id)
let response = get_http_response(&window, response_id)
.await
.expect("Failed to get http response");
@@ -772,26 +801,48 @@ async fn cmd_filter_response(
}
}
let body = read_to_string(response.body_path.unwrap()).unwrap();
let body = read_to_string(response.body_path.unwrap()).await.unwrap();
// TODO: Have plugins register their own content type (regex?)
plugin_manager
.filter_data(filter, &body, &content_type)
.filter_data(&window, filter, &body, &content_type)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_import_data(
w: WebviewWindow,
async fn cmd_get_sse_events(file_path: &str) -> Result<Vec<ServerSentEvent>, String> {
let body = fs::read(file_path).map_err(|e| e.to_string())?;
let mut p = EventParser::new();
p.process_bytes(body.into()).map_err(|e| e.to_string())?;
let mut events = Vec::new();
while let Some(e) = p.get_event() {
if let SSE::Event(e) = e {
events.push(ServerSentEvent {
event_type: e.event_type,
data: e.data,
id: e.id,
retry: e.retry,
});
}
}
Ok(events)
}
#[tauri::command]
async fn cmd_import_data<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
file_path: &str,
) -> Result<WorkspaceExportResources, String> {
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file = read_to_string(file_path)
.await
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let (import_result, plugin_name) = plugin_manager
.import_data(file_contents)
.import_data(&window, file_contents)
.await
.map_err(|e| e.to_string())?;
@@ -828,7 +879,9 @@ async fn cmd_import_data(
for mut v in resources.workspaces {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeWorkspace, &mut id_map);
let x = upsert_workspace(&w, v).await.map_err(|e| e.to_string())?;
let x = upsert_workspace(&window, v)
.await
.map_err(|e| e.to_string())?;
imported_resources.workspaces.push(x.clone());
}
info!(
@@ -843,7 +896,9 @@ async fn cmd_import_data(
ModelType::TypeWorkspace,
&mut id_map,
);
let x = upsert_environment(&w, v).await.map_err(|e| e.to_string())?;
let x = upsert_environment(&window, v)
.await
.map_err(|e| e.to_string())?;
imported_resources.environments.push(x.clone());
}
info!(
@@ -875,7 +930,7 @@ async fn cmd_import_data(
if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) {
continue;
}
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
let x = upsert_folder(&window, v).await.map_err(|e| e.to_string())?;
imported_resources.folders.push(x.clone());
}
}
@@ -889,7 +944,7 @@ async fn cmd_import_data(
&mut id_map,
);
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
let x = upsert_http_request(&w, v)
let x = upsert_http_request(&window, v)
.await
.map_err(|e| e.to_string())?;
imported_resources.http_requests.push(x.clone());
@@ -907,7 +962,7 @@ async fn cmd_import_data(
&mut id_map,
);
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
let x = upsert_grpc_request(&w, &v)
let x = upsert_grpc_request(&window, &v)
.await
.map_err(|e| e.to_string())?;
imported_resources.grpc_requests.push(x.clone());
@@ -918,7 +973,7 @@ async fn cmd_import_data(
);
analytics::track_event(
&w,
&window,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -929,52 +984,55 @@ async fn cmd_import_data(
}
#[tauri::command]
async fn cmd_http_request_actions(
async fn cmd_http_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpRequestActionsResponse>, String> {
plugin_manager
.get_http_request_actions()
.get_http_request_actions(&window)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_template_functions(
async fn cmd_template_functions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetTemplateFunctionsResponse>, String> {
plugin_manager
.get_template_functions()
.get_template_functions(&window)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action(
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallHttpRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.call_http_request_action(req)
.call_http_request_action(&window, req)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_curl_to_request(
async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>,
command: &str,
plugin_manager: State<'_, PluginManager>,
workspace_id: &str,
w: WebviewWindow,
) -> Result<HttpRequest, String> {
let (import_result, plugin_name) = {
plugin_manager
.import_data(command)
.import_data(&window, command)
.await
.map_err(|e| e.to_string())?
};
analytics::track_event(
&w,
&window,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -1057,6 +1115,20 @@ async fn cmd_send_http_request(
// that has not yet been saved in the DB.
request: HttpRequest,
) -> Result<HttpResponse, String> {
let response = create_default_http_response(&window, &request.id)
.await
.map_err(|e| e.to_string())?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(
format!("cancel_http_response_{}", response.id),
move |_event| {
if let Err(e) = cancel_tx.send(true) {
warn!("Failed to send cancel event for request {e:?}");
}
},
);
let environment = match environment_id {
Some(id) => match get_environment(&window, id).await {
Ok(env) => Some(env),
@@ -1077,18 +1149,6 @@ async fn cmd_send_http_request(
None => None,
};
let response = create_default_http_response(&window, &request.id)
.await
.map_err(|e| e.to_string())?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(
format!("cancel_http_response_{}", response.id),
move |_event| {
let _ = cancel_tx.send(true);
},
);
send_http_request(
&window,
&request,
@@ -1104,15 +1164,15 @@ async fn response_err<R: Runtime>(
response: &HttpResponse,
error: String,
w: &WebviewWindow<R>,
) -> Result<HttpResponse, String> {
warn!("Failed to send request: {}", error);
) -> HttpResponse {
warn!("Failed to send request: {error:?}");
let mut response = response.clone();
response.elapsed = -1;
response.state = HttpResponseState::Closed;
response.error = Some(error.clone());
response = update_response_if_id(w, &response)
.await
.expect("Failed to update response");
Ok(response)
response
}
#[tauri::command]
@@ -1176,19 +1236,19 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
}
#[tauri::command]
async fn cmd_install_plugin(
async fn cmd_install_plugin<R: Runtime>(
directory: &str,
url: Option<String>,
plugin_manager: State<'_, PluginManager>,
w: WebviewWindow,
window: WebviewWindow<R>,
) -> Result<Plugin, String> {
plugin_manager
.add_plugin_by_dir(&directory)
.add_plugin_by_dir(WindowContext::from_window(&window), &directory, true)
.await
.map_err(|e| e.to_string())?;
let plugin = upsert_plugin(
&w,
&window,
Plugin {
directory: directory.into(),
url,
@@ -1202,17 +1262,20 @@ async fn cmd_install_plugin(
}
#[tauri::command]
async fn cmd_uninstall_plugin(
async fn cmd_uninstall_plugin<R: Runtime>(
plugin_id: &str,
plugin_manager: State<'_, PluginManager>,
w: WebviewWindow,
window: WebviewWindow<R>,
) -> Result<Plugin, String> {
let plugin = delete_plugin(&w, plugin_id)
let plugin = delete_plugin(&window, plugin_id)
.await
.map_err(|e| e.to_string())?;
plugin_manager
.uninstall(plugin.directory.as_str())
.uninstall(
WindowContext::from_window(&window),
plugin.directory.as_str(),
)
.await
.map_err(|e| e.to_string())?;
@@ -1439,10 +1502,10 @@ async fn cmd_delete_environment(
#[tauri::command]
async fn cmd_list_grpc_connections(
request_id: &str,
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<GrpcConnection>, String> {
list_grpc_connections(&w, request_id)
list_grpc_connections(&w, workspace_id)
.await
.map_err(|e| e.to_string())
}
@@ -1493,12 +1556,12 @@ async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
}
#[tauri::command]
async fn cmd_reload_plugins(
app_handle: AppHandle,
async fn cmd_reload_plugins<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.initialize_all_plugins(&app_handle)
.initialize_all_plugins(window.app_handle(), WindowContext::from_window(&window))
.await
.map_err(|e| e.to_string())?;
Ok(())
@@ -1511,13 +1574,12 @@ async fn cmd_plugin_info(
plugin_manager: State<'_, PluginManager>,
) -> Result<BootResponse, String> {
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
plugin_manager
Ok(plugin_manager
.get_plugin_by_dir(plugin.directory.as_str())
.await
.ok_or("Failed to find plugin info".to_string())?
.ok_or("Failed to find plugin for info".to_string())?
.info()
.await
.ok_or("Failed to find plugin".to_string())
.await)
}
#[tauri::command]
@@ -1590,11 +1652,11 @@ async fn cmd_get_workspace(id: &str, w: WebviewWindow) -> Result<Workspace, Stri
#[tauri::command]
async fn cmd_list_http_responses(
request_id: &str,
workspace_id: &str,
limit: Option<i64>,
w: WebviewWindow,
) -> Result<Vec<HttpResponse>, String> {
list_http_responses(&w, request_id, limit)
list_http_responses(&w, workspace_id, limit)
.await
.map_err(|e| e.to_string())
}
@@ -1622,7 +1684,7 @@ async fn cmd_delete_all_grpc_connections(request_id: &str, w: WebviewWindow) ->
#[tauri::command]
async fn cmd_delete_all_http_responses(request_id: &str, w: WebviewWindow) -> Result<(), String> {
delete_all_http_responses(&w, request_id)
delete_all_http_responses_for_request(&w, request_id)
.await
.map_err(|e| e.to_string())
}
@@ -1765,6 +1827,7 @@ pub fn run() {
])
.level_for("plugin_runtime", log::LevelFilter::Info)
.level_for("cookie_store", log::LevelFilter::Info)
.level_for("eventsource_client::event_parser", log::LevelFilter::Info)
.level_for("h2", log::LevelFilter::Info)
.level_for("hyper", log::LevelFilter::Info)
.level_for("hyper_util", log::LevelFilter::Info)
@@ -1829,10 +1892,6 @@ pub fn run() {
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Plugin template callback
let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone());
app.manage(plugin_cb);
monitor_plugin_events(&app.app_handle().clone());
Ok(())
@@ -1869,6 +1928,7 @@ pub fn run() {
cmd_get_folder,
cmd_get_grpc_request,
cmd_get_http_request,
cmd_get_sse_events,
cmd_get_key_value,
cmd_get_settings,
cmd_get_workspace,
@@ -2138,12 +2198,50 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
});
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct FrontendCall<T: Serialize + Clone> {
args: T,
reply_id: String,
}
async fn call_frontend<T: Serialize + Clone, R: Runtime>(
window: WebviewWindow<R>,
event_name: &str,
args: T,
) -> PromptTextResponse {
let reply_id = format!("{event_name}_reply_{}", generate_id());
let payload = FrontendCall {
args,
reply_id: reply_id.clone(),
};
window.emit_to(window.label(), event_name, payload).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(PromptTextResponse::default());
let event_id = window.clone().listen(reply_id, move |ev| {
let resp: PromptTextResponse = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(resp) {
warn!("Failed to prompt for text {e:?}");
}
});
// When reply shows up, unlisten to events and return
if let Err(e) = rx.changed().await {
warn!("Failed to check channel changed {e:?}");
}
window.unlisten(event_id);
let foo = rx.borrow();
foo.clone()
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
@@ -2153,13 +2251,24 @@ async fn handle_plugin_event<R: Runtime>(
None
}
InternalEventPayload::ShowToastRequest(req) => {
app_handle
.emit("show_toast", req)
.expect("Failed to emit show_toast");
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle
.emit("show_toast", req)
.expect("Failed to emit show_toast"),
};
None
}
InternalEventPayload::PromptTextRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let resp = call_frontend(window, "show_prompt", req).await;
Some(InternalEventPayload::PromptTextResponse(resp))
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses(
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
@@ -2177,32 +2286,38 @@ async fn handle_plugin_event<R: Runtime>(
))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_focused_window_no_lock(app_handle).expect("No focused window");
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
.await
.expect("Failed to get workspace for request");
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let url = window.url().unwrap();
let mut query_pairs = url.query_pairs();
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(&window, id.as_str()).await.ok(),
};
let cb = &*app_handle.state::<PluginTemplateCallback>();
let rendered_http_request =
render_http_request(&req.http_request, &workspace, environment.as_ref(), cb).await;
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request =
render_http_request(&req.http_request, &workspace, environment.as_ref(), &cb).await;
Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse {
http_request: rendered_http_request,
},
RenderHttpRequestResponse { http_request },
))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data = render_json_value(req.data, &workspace, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(
TemplateRenderResponse { data },
))
}
InternalEventPayload::ReloadResponse => {
let window = get_focused_window_no_lock(app_handle).expect("No focused window");
let plugins = list_plugins(&window).await.unwrap();
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
@@ -2215,6 +2330,7 @@ async fn handle_plugin_event<R: Runtime>(
upsert_plugin(&window, new_plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
@@ -2226,32 +2342,17 @@ async fn handle_plugin_event<R: Runtime>(
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
let cookie_jar_id = query_pairs
.find(|(k, _v)| k == "cookie_jar_id")
.map(|(_k, v)| v.to_string());
let cookie_jar = match cookie_jar_id {
None => None,
Some(id) => get_cookie_jar(app_handle, id.as_str()).await.ok(),
};
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(app_handle, id.as_str()).await.ok(),
};
let resp = create_default_http_response(&w, req.http_request.id.as_str())
let resp = create_default_http_response(&window, req.http_request.id.as_str())
.await
.unwrap();
let result = send_http_request(
&w,
&window,
&req.http_request,
&resp,
environment,
@@ -2280,29 +2381,78 @@ async fn handle_plugin_event<R: Runtime>(
}
}
// app_handle.get_focused_window locks, so this one is a non-locking version, safe for use in async context
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
// TODO: Getting the focused window doesn't seem to work on Windows, so
// we'll need to pass the window label into plugin events instead.
let main_windows = app_handle
.webview_windows()
.iter()
.filter_map(|(_, w)| {
if w.label().starts_with(MAIN_WINDOW_PREFIX) {
Some(w.to_owned())
} else {
None
}
})
.collect::<Vec<WebviewWindow<R>>>();
fn get_window_from_window_context<R: Runtime>(
app_handle: &AppHandle<R>,
window_context: &WindowContext,
) -> Option<WebviewWindow<R>> {
let label = match window_context {
WindowContext::Label { label } => label,
WindowContext::None => {
return app_handle
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned());
}
};
if main_windows.len() == 1 {
return main_windows.iter().next().map(|w| w.clone());
let window = app_handle.webview_windows().iter().find_map(|(_, w)| {
if w.label() == label {
Some(w.to_owned())
} else {
None
}
});
if window.is_none() {
error!("Failed to find window by {window_context:?}");
}
main_windows
.iter()
.cloned()
.find(|w| w.is_focused().unwrap_or(false))
.map(|w| w.clone())
window
}
fn workspace_id_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<String> {
let url = window.url().unwrap();
let re = Regex::new(r"/workspaces/(?<wid>\w+)").unwrap();
match re.captures(url.as_str()) {
None => None,
Some(captures) => captures.name("wid").map(|c| c.as_str().to_string()),
}
}
async fn workspace_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Workspace> {
match workspace_id_from_window(&window) {
None => None,
Some(id) => get_workspace(window, id.as_str()).await.ok(),
}
}
fn environment_id_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<String> {
let url = window.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string())
}
async fn environment_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Environment> {
match environment_id_from_window(&window) {
None => None,
Some(id) => get_environment(window, id.as_str()).await.ok(),
}
}
fn cookie_jar_id_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<String> {
let url = window.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs
.find(|(k, _v)| k == "cookie_jar_id")
.map(|(_k, v)| v.to_string())
}
async fn cookie_jar_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<CookieJar> {
match cookie_jar_id_from_window(&window) {
None => None,
Some(id) => get_cookie_jar(window, id.as_str()).await.ok(),
}
}

View File

@@ -51,7 +51,7 @@ impl YaakNotifier {
Ok(())
}
pub async fn check<R: Runtime>(&mut self, w: &WebviewWindow<R>) -> Result<(), String> {
pub async fn check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<(), String> {
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
@@ -60,8 +60,8 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let num_launches = get_num_launches(w).await;
let info = w.app_handle().package_info().clone();
let num_launches = get_num_launches(window).await;
let info = window.app_handle().package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
@@ -80,14 +80,14 @@ impl YaakNotifier {
.map_err(|e| e.to_string())?;
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(w).await?;
let seen = get_kv(window).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
}
debug!("Got notification {:?}", notification);
let _ = w.emit("notification", notification.clone());
let _ = window.emit_to(window.label(), "notification", notification.clone());
Ok(())
}

View File

@@ -1,31 +1,38 @@
use crate::template_callback::PluginTemplateCallback;
use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, HashMap};
use tauri::{AppHandle, Manager, Runtime};
use yaak_models::models::{
Environment, EnvironmentVariable, GrpcMetadataEntry, GrpcRequest, HttpRequest,
HttpRequestHeader, HttpUrlParameter, Workspace,
};
use yaak_templates::{parse_and_render, TemplateCallback};
pub async fn render_template<R: Runtime>(
app_handle: &AppHandle<R>,
pub async fn render_template<T: TemplateCallback>(
template: &str,
w: &Workspace,
e: Option<&Environment>,
cb: &T,
) -> String {
let cb = &*app_handle.state::<PluginTemplateCallback>();
let vars = &make_vars_hashmap(w, e);
render(template, vars, cb).await
}
pub async fn render_grpc_request<R: Runtime>(
app_handle: &AppHandle<R>,
pub async fn render_json_value<T: TemplateCallback>(
value: Value,
w: &Workspace,
e: Option<&Environment>,
cb: &T,
) -> Value {
let vars = &make_vars_hashmap(w, e);
render_json_value_raw(value, vars, cb).await
}
pub async fn render_grpc_request<T: TemplateCallback>(
r: &GrpcRequest,
w: &Workspace,
e: Option<&Environment>,
cb: &T,
) -> GrpcRequest {
let cb = &*app_handle.state::<PluginTemplateCallback>();
let vars = &make_vars_hashmap(w, e);
let mut metadata = Vec::new();
@@ -39,7 +46,7 @@ pub async fn render_grpc_request<R: Runtime>(
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value(v, vars, cb).await);
authentication.insert(k, render_json_value_raw(v, vars, cb).await);
}
let url = render(r.url.as_str(), vars, cb).await;
@@ -80,23 +87,26 @@ pub async fn render_http_request(
let mut body = BTreeMap::new();
for (k, v) in r.body.clone() {
body.insert(k, render_json_value(v, vars, cb).await);
body.insert(k, render_json_value_raw(v, vars, cb).await);
}
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value(v, vars, cb).await);
authentication.insert(k, render_json_value_raw(v, vars, cb).await);
}
let url = render(r.url.clone().as_str(), vars, cb).await;
HttpRequest {
let req = HttpRequest {
url,
url_parameters,
headers,
body,
authentication,
..r.to_owned()
}
};
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
apply_path_placeholders(req)
}
pub fn make_vars_hashmap(
@@ -138,7 +148,7 @@ fn add_variable_to_map(
map
}
pub async fn render_json_value<T: TemplateCallback>(
async fn render_json_value_raw<T: TemplateCallback>(
v: Value,
vars: &HashMap<String, String>,
cb: &T,
@@ -148,7 +158,7 @@ pub async fn render_json_value<T: TemplateCallback>(
Value::Array(a) => {
let mut new_a = Vec::new();
for v in a {
new_a.push(Box::pin(render_json_value(v, vars, cb)).await)
new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await)
}
json!(new_a)
}
@@ -156,7 +166,7 @@ pub async fn render_json_value<T: TemplateCallback>(
let mut new_o = Map::new();
for (k, v) in o {
let key = Box::pin(render(k.as_str(), vars, cb)).await;
let value = Box::pin(render_json_value(v, vars, cb)).await;
let value = Box::pin(render_json_value_raw(v, vars, cb)).await;
new_o.insert(key, value);
}
json!(new_o)
@@ -166,7 +176,7 @@ pub async fn render_json_value<T: TemplateCallback>(
}
#[cfg(test)]
mod tests {
mod render_tests {
use serde_json::json;
use std::collections::HashMap;
use yaak_templates::TemplateCallback;
@@ -189,7 +199,7 @@ mod tests {
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!("aaa"))
}
@@ -199,7 +209,7 @@ mod tests {
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!(["aaa", "aaa"]))
}
@@ -209,7 +219,7 @@ mod tests {
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!({"aaa": "aaa"}))
}
@@ -226,7 +236,7 @@ mod tests {
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(
result,
json!([
@@ -240,3 +250,173 @@ mod tests {
)
}
}
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
if !p.enabled {
return url.to_string();
}
if !p.name.starts_with(":") {
return url.to_string();
}
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
let result = re
.replace_all(url, |cap: &regex::Captures| {
format!(
"{}{}{}",
cap[1].to_string(),
urlencoding::encode(p.value.as_str()),
cap[2].to_string()
)
})
.into_owned();
result
}
fn apply_path_placeholders(rendered_request: HttpRequest) -> HttpRequest {
let mut url = rendered_request.url.to_owned();
let mut url_parameters = Vec::new();
for p in rendered_request.url_parameters.clone() {
if !p.enabled || p.name.is_empty() {
continue;
}
// Replace path parameters with values from URL parameters
let old_url_string = url.clone();
url = replace_path_placeholder(&p, url.as_str());
// Remove as param if it modified the URL
if old_url_string == url {
url_parameters.push(p);
}
}
let mut request = rendered_request.clone();
request.url_parameters = url_parameters;
request.url = url;
request
}
#[cfg(test)]
mod placeholder_tests {
use crate::render::{apply_path_placeholders, replace_path_placeholder};
use yaak_models::models::{HttpRequest, HttpUrlParameter};
#[test]
fn placeholder_middle() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
"https://example.com/xxx/bar",
);
}
#[test]
fn placeholder_end() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/xxx",
);
}
#[test]
fn placeholder_query() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
"https://example.com/xxx?:foo",
);
}
#[test]
fn placeholder_missing() {
let p = HttpUrlParameter {
enabled: true,
name: "".to_string(),
value: "".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
"https://example.com/:missing",
);
}
#[test]
fn placeholder_disabled() {
let p = HttpUrlParameter {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/:foo",
);
}
#[test]
fn placeholder_prefix() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
"https://example.com/:foooo",
);
}
#[test]
fn placeholder_encode() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/Hello%20World",
);
}
#[test]
fn apply_placeholder() {
let result = apply_path_placeholders(HttpRequest {
url: "example.com/:a/bar".to_string(),
url_parameters: vec![
HttpUrlParameter {
name: "b".to_string(),
value: "bbb".to_string(),
enabled: true,
},
HttpUrlParameter {
name: ":a".to_string(),
value: "aaa".to_string(),
enabled: true,
},
],
..Default::default()
});
println!("HELLO?: {result:?}");
assert_eq!(result.url, "example.com/aaa/bar");
assert_eq!(result.url_parameters.len(), 1);
assert_eq!(result.url_parameters[0].name, "b");
assert_eq!(result.url_parameters[0].value, "bbb");
}
}

View File

@@ -1,32 +1,34 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager};
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg};
use tauri::{AppHandle, Manager, Runtime};
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg, WindowContext};
use yaak_plugin_runtime::manager::PluginManager;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
pub struct PluginTemplateCallback {
app_handle: AppHandle,
purpose: RenderPurpose,
plugin_manager: PluginManager,
window_context: WindowContext,
render_purpose: RenderPurpose,
}
impl PluginTemplateCallback {
pub fn new(app_handle: AppHandle) -> PluginTemplateCallback {
pub fn new<R: Runtime>(
app_handle: &AppHandle<R>,
window_context: &WindowContext,
render_purpose: RenderPurpose,
) -> PluginTemplateCallback {
let plugin_manager = &*app_handle.state::<PluginManager>();
PluginTemplateCallback {
app_handle,
purpose: RenderPurpose::Preview,
plugin_manager: plugin_manager.to_owned(),
window_context: window_context.to_owned(),
render_purpose,
}
}
pub fn for_send(&self) -> PluginTemplateCallback {
let mut v = self.clone();
v.purpose = RenderPurpose::Send;
v
}
}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
let window_context = self.window_context.to_owned();
// 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" {
@@ -35,9 +37,9 @@ impl TemplateCallback for PluginTemplateCallback {
fn_name
};
let plugin_manager = self.app_handle.state::<PluginManager>();
let function = plugin_manager
.get_template_functions()
let function = self
.plugin_manager
.get_template_functions_with_context(window_context.to_owned())
.await
.map_err(|e| e.to_string())?
.iter()
@@ -53,6 +55,7 @@ impl TemplateCallback for PluginTemplateCallback {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::File(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
@@ -60,8 +63,14 @@ impl TemplateCallback for PluginTemplateCallback {
}
}
let resp = plugin_manager
.call_template_function(fn_name, args_with_defaults, self.purpose.clone())
let resp = self
.plugin_manager
.call_template_function(
window_context,
fn_name,
args_with_defaults,
self.render_purpose.to_owned(),
)
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())

View File

@@ -3,7 +3,7 @@ use std::time::SystemTime;
use log::info;
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;
use tokio::task::block_in_place;
use yaak_plugin_runtime::manager::PluginManager;
@@ -92,8 +92,10 @@ impl YaakUpdater {
"{} is available. Would you like to download and install it now?",
update.version
))
.ok_button_label("Download")
.cancel_button_label("Later")
.buttons(MessageDialogButtons::OkCancelCustom(
"Download".to_string(),
"Later".to_string(),
))
.title("Update Available")
.show(|confirmed| {
if !confirmed {
@@ -105,8 +107,10 @@ impl YaakUpdater {
if h.dialog()
.message("Would you like to restart the app?")
.title("Update Installed")
.ok_button_label("Restart")
.cancel_button_label("Later")
.buttons(MessageDialogButtons::OkCancelCustom(
"Restart".to_string(),
"Later".to_string(),
))
.blocking_show()
{
h.restart();

View File

@@ -76,6 +76,7 @@
"developmentTeam": "7PU3P6ELJ8"
},
"macOS": {
"minimumSystemVersion": "13.0",
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []

View File

@@ -0,0 +1,98 @@
"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,
pluginHookExport: () => pluginHookExport
});
module.exports = __toCommonJS(src_exports);
var NEWLINE = "\\\n ";
var plugin = {
httpRequestActions: [{
key: "export-curl",
label: "Copy as Curl",
icon: "copy",
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" });
const data = await pluginHookExport(ctx, rendered_request);
ctx.clipboard.copyText(data);
ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy" });
}
}]
};
async function pluginHookExport(_ctx, request) {
const xs = ["curl"];
if (request.method) xs.push("-X", request.method);
if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE);
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
xs.push("--url-query", quote(`${p.name}=${p.value}`));
xs.push(NEWLINE);
}
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push("--header", quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
if (Array.isArray(request.body?.form)) {
const flag = request.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : "";
xs.push(flag, v);
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.text === "string") {
xs.push("--data-raw", `$${quote(request.body.text)}`);
xs.push(NEWLINE);
}
if (request.authenticationType === "basic" || request.authenticationType === "digest") {
if (request.authenticationType === "digest") xs.push("--digest");
xs.push(
"--user",
quote(`${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`)
);
xs.push(NEWLINE);
}
if (request.authenticationType === "bearer") {
xs.push("--header", quote(`Authorization: Bearer ${request.authentication?.token ?? ""}`));
xs.push(NEWLINE);
}
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(" ");
}
function quote(arg) {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v) {
return v.enabled !== false && !!v.name;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin,
pluginHookExport
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/exporter-curl",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,510 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookResponseFilter: () => pluginHookResponseFilter
});
module.exports = __toCommonJS(src_exports);
// ../../node_modules/jsonpath-plus/dist/index-node-esm.js
var import_vm = __toESM(require("vm"), 1);
var {
hasOwnProperty: hasOwnProp
} = Object.prototype;
function push(arr, item) {
arr = arr.slice();
arr.push(item);
return arr;
}
function unshift(item, arr) {
arr = arr.slice();
arr.unshift(item);
return arr;
}
var NewError = class extends Error {
/**
* @param {AnyResult} value The evaluated scalar value
*/
constructor(value) {
super('JSONPath should not be called with "new" (it prevents return of (unwrapped) scalar values)');
this.avoidNew = true;
this.value = value;
this.name = "NewError";
}
};
function JSONPath(opts, expr, obj, callback, otherTypeCallback) {
if (!(this instanceof JSONPath)) {
try {
return new JSONPath(opts, expr, obj, callback, otherTypeCallback);
} catch (e) {
if (!e.avoidNew) {
throw e;
}
return e.value;
}
}
if (typeof opts === "string") {
otherTypeCallback = callback;
callback = obj;
obj = expr;
expr = opts;
opts = null;
}
const optObj = opts && typeof opts === "object";
opts = opts || {};
this.json = opts.json || obj;
this.path = opts.path || expr;
this.resultType = opts.resultType || "value";
this.flatten = opts.flatten || false;
this.wrap = hasOwnProp.call(opts, "wrap") ? opts.wrap : true;
this.sandbox = opts.sandbox || {};
this.eval = opts.eval === void 0 ? "safe" : opts.eval;
this.ignoreEvalErrors = typeof opts.ignoreEvalErrors === "undefined" ? false : opts.ignoreEvalErrors;
this.parent = opts.parent || null;
this.parentProperty = opts.parentProperty || null;
this.callback = opts.callback || callback || null;
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function() {
throw new TypeError("You must supply an otherTypeCallback callback option with the @other() operator.");
};
if (opts.autostart !== false) {
const args = {
path: optObj ? opts.path : expr
};
if (!optObj) {
args.json = obj;
} else if ("json" in opts) {
args.json = opts.json;
}
const ret = this.evaluate(args);
if (!ret || typeof ret !== "object") {
throw new NewError(ret);
}
return ret;
}
}
JSONPath.prototype.evaluate = function(expr, json, callback, otherTypeCallback) {
let currParent = this.parent, currParentProperty = this.parentProperty;
let {
flatten,
wrap
} = this;
this.currResultType = this.resultType;
this.currEval = this.eval;
this.currSandbox = this.sandbox;
callback = callback || this.callback;
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
json = json || this.json;
expr = expr || this.path;
if (expr && typeof expr === "object" && !Array.isArray(expr)) {
if (!expr.path && expr.path !== "") {
throw new TypeError('You must supply a "path" property when providing an object argument to JSONPath.evaluate().');
}
if (!hasOwnProp.call(expr, "json")) {
throw new TypeError('You must supply a "json" property when providing an object argument to JSONPath.evaluate().');
}
({
json
} = expr);
flatten = hasOwnProp.call(expr, "flatten") ? expr.flatten : flatten;
this.currResultType = hasOwnProp.call(expr, "resultType") ? expr.resultType : this.currResultType;
this.currSandbox = hasOwnProp.call(expr, "sandbox") ? expr.sandbox : this.currSandbox;
wrap = hasOwnProp.call(expr, "wrap") ? expr.wrap : wrap;
this.currEval = hasOwnProp.call(expr, "eval") ? expr.eval : this.currEval;
callback = hasOwnProp.call(expr, "callback") ? expr.callback : callback;
this.currOtherTypeCallback = hasOwnProp.call(expr, "otherTypeCallback") ? expr.otherTypeCallback : this.currOtherTypeCallback;
currParent = hasOwnProp.call(expr, "parent") ? expr.parent : currParent;
currParentProperty = hasOwnProp.call(expr, "parentProperty") ? expr.parentProperty : currParentProperty;
expr = expr.path;
}
currParent = currParent || null;
currParentProperty = currParentProperty || null;
if (Array.isArray(expr)) {
expr = JSONPath.toPathString(expr);
}
if (!expr && expr !== "" || !json) {
return void 0;
}
const exprList = JSONPath.toPathArray(expr);
if (exprList[0] === "$" && exprList.length > 1) {
exprList.shift();
}
this._hasParentSelector = null;
const result = this._trace(exprList, json, ["$"], currParent, currParentProperty, callback).filter(function(ea) {
return ea && !ea.isParentSelector;
});
if (!result.length) {
return wrap ? [] : void 0;
}
if (!wrap && result.length === 1 && !result[0].hasArrExpr) {
return this._getPreferredOutput(result[0]);
}
return result.reduce((rslt, ea) => {
const valOrPath = this._getPreferredOutput(ea);
if (flatten && Array.isArray(valOrPath)) {
rslt = rslt.concat(valOrPath);
} else {
rslt.push(valOrPath);
}
return rslt;
}, []);
};
JSONPath.prototype._getPreferredOutput = function(ea) {
const resultType = this.currResultType;
switch (resultType) {
case "all": {
const path = Array.isArray(ea.path) ? ea.path : JSONPath.toPathArray(ea.path);
ea.pointer = JSONPath.toPointer(path);
ea.path = typeof ea.path === "string" ? ea.path : JSONPath.toPathString(ea.path);
return ea;
}
case "value":
case "parent":
case "parentProperty":
return ea[resultType];
case "path":
return JSONPath.toPathString(ea[resultType]);
case "pointer":
return JSONPath.toPointer(ea.path);
default:
throw new TypeError("Unknown result type");
}
};
JSONPath.prototype._handleCallback = function(fullRetObj, callback, type) {
if (callback) {
const preferredOutput = this._getPreferredOutput(fullRetObj);
fullRetObj.path = typeof fullRetObj.path === "string" ? fullRetObj.path : JSONPath.toPathString(fullRetObj.path);
callback(preferredOutput, type, fullRetObj);
}
};
JSONPath.prototype._trace = function(expr, val, path, parent, parentPropName, callback, hasArrExpr, literalPriority) {
let retObj;
if (!expr.length) {
retObj = {
path,
value: val,
parent,
parentProperty: parentPropName,
hasArrExpr
};
this._handleCallback(retObj, callback, "value");
return retObj;
}
const loc = expr[0], x = expr.slice(1);
const ret = [];
function addRet(elems) {
if (Array.isArray(elems)) {
elems.forEach((t) => {
ret.push(t);
});
} else {
ret.push(elems);
}
}
if ((typeof loc !== "string" || literalPriority) && val && hasOwnProp.call(val, loc)) {
addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, hasArrExpr));
} else if (loc === "*") {
this._walk(val, (m) => {
addRet(this._trace(x, val[m], push(path, m), val, m, callback, true, true));
});
} else if (loc === "..") {
addRet(this._trace(x, val, path, parent, parentPropName, callback, hasArrExpr));
this._walk(val, (m) => {
if (typeof val[m] === "object") {
addRet(this._trace(expr.slice(), val[m], push(path, m), val, m, callback, true));
}
});
} else if (loc === "^") {
this._hasParentSelector = true;
return {
path: path.slice(0, -1),
expr: x,
isParentSelector: true
};
} else if (loc === "~") {
retObj = {
path: push(path, loc),
value: parentPropName,
parent,
parentProperty: null
};
this._handleCallback(retObj, callback, "property");
return retObj;
} else if (loc === "$") {
addRet(this._trace(x, val, path, null, null, callback, hasArrExpr));
} else if (/^(-?\d*):(-?\d*):?(\d*)$/u.test(loc)) {
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
} else if (loc.indexOf("?(") === 0) {
if (this.currEval === false) {
throw new Error("Eval [?(expr)] prevented in JSONPath expression.");
}
const safeLoc = loc.replace(/^\?\((.*?)\)$/u, "$1");
const nested = /@.?([^?]*)[['](\??\(.*?\))(?!.\)\])[\]']/gu.exec(safeLoc);
if (nested) {
this._walk(val, (m) => {
const npath = [nested[2]];
const nvalue = nested[1] ? val[m][nested[1]] : val[m];
const filterResults = this._trace(npath, nvalue, path, parent, parentPropName, callback, true);
if (filterResults.length > 0) {
addRet(this._trace(x, val[m], push(path, m), val, m, callback, true));
}
});
} else {
this._walk(val, (m) => {
if (this._eval(safeLoc, val[m], m, path, parent, parentPropName)) {
addRet(this._trace(x, val[m], push(path, m), val, m, callback, true));
}
});
}
} else if (loc[0] === "(") {
if (this.currEval === false) {
throw new Error("Eval [(expr)] prevented in JSONPath expression.");
}
addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback, hasArrExpr));
} else if (loc[0] === "@") {
let addType = false;
const valueType = loc.slice(1, -2);
switch (valueType) {
case "scalar":
if (!val || !["object", "function"].includes(typeof val)) {
addType = true;
}
break;
case "boolean":
case "string":
case "undefined":
case "function":
if (typeof val === valueType) {
addType = true;
}
break;
case "integer":
if (Number.isFinite(val) && !(val % 1)) {
addType = true;
}
break;
case "number":
if (Number.isFinite(val)) {
addType = true;
}
break;
case "nonFinite":
if (typeof val === "number" && !Number.isFinite(val)) {
addType = true;
}
break;
case "object":
if (val && typeof val === valueType) {
addType = true;
}
break;
case "array":
if (Array.isArray(val)) {
addType = true;
}
break;
case "other":
addType = this.currOtherTypeCallback(val, path, parent, parentPropName);
break;
case "null":
if (val === null) {
addType = true;
}
break;
default:
throw new TypeError("Unknown value type " + valueType);
}
if (addType) {
retObj = {
path,
value: val,
parent,
parentProperty: parentPropName
};
this._handleCallback(retObj, callback, "value");
return retObj;
}
} else if (loc[0] === "`" && val && hasOwnProp.call(val, loc.slice(1))) {
const locProp = loc.slice(1);
addRet(this._trace(x, val[locProp], push(path, locProp), val, locProp, callback, hasArrExpr, true));
} else if (loc.includes(",")) {
const parts = loc.split(",");
for (const part of parts) {
addRet(this._trace(unshift(part, x), val, path, parent, parentPropName, callback, true));
}
} else if (!literalPriority && val && hasOwnProp.call(val, loc)) {
addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, hasArrExpr, true));
}
if (this._hasParentSelector) {
for (let t = 0; t < ret.length; t++) {
const rett = ret[t];
if (rett && rett.isParentSelector) {
const tmp = this._trace(rett.expr, val, rett.path, parent, parentPropName, callback, hasArrExpr);
if (Array.isArray(tmp)) {
ret[t] = tmp[0];
const tl = tmp.length;
for (let tt = 1; tt < tl; tt++) {
t++;
ret.splice(t, 0, tmp[tt]);
}
} else {
ret[t] = tmp;
}
}
}
}
return ret;
};
JSONPath.prototype._walk = function(val, f) {
if (Array.isArray(val)) {
const n = val.length;
for (let i = 0; i < n; i++) {
f(i);
}
} else if (val && typeof val === "object") {
Object.keys(val).forEach((m) => {
f(m);
});
}
};
JSONPath.prototype._slice = function(loc, expr, val, path, parent, parentPropName, callback) {
if (!Array.isArray(val)) {
return void 0;
}
const len = val.length, parts = loc.split(":"), step = parts[2] && Number.parseInt(parts[2]) || 1;
let start = parts[0] && Number.parseInt(parts[0]) || 0, end = parts[1] && Number.parseInt(parts[1]) || len;
start = start < 0 ? Math.max(0, start + len) : Math.min(len, start);
end = end < 0 ? Math.max(0, end + len) : Math.min(len, end);
const ret = [];
for (let i = start; i < end; i += step) {
const tmp = this._trace(unshift(i, expr), val, path, parent, parentPropName, callback, true);
tmp.forEach((t) => {
ret.push(t);
});
}
return ret;
};
JSONPath.prototype._eval = function(code, _v, _vname, path, parent, parentPropName) {
this.currSandbox._$_parentProperty = parentPropName;
this.currSandbox._$_parent = parent;
this.currSandbox._$_property = _vname;
this.currSandbox._$_root = this.json;
this.currSandbox._$_v = _v;
const containsPath = code.includes("@path");
if (containsPath) {
this.currSandbox._$_path = JSONPath.toPathString(path.concat([_vname]));
}
const scriptCacheKey = this.currEval + "Script:" + code;
if (!JSONPath.cache[scriptCacheKey]) {
let script = code.replace(/@parentProperty/gu, "_$_parentProperty").replace(/@parent/gu, "_$_parent").replace(/@property/gu, "_$_property").replace(/@root/gu, "_$_root").replace(/@([.\s)[])/gu, "_$_v$1");
if (containsPath) {
script = script.replace(/@path/gu, "_$_path");
}
if (this.currEval === "safe" || this.currEval === true || this.currEval === void 0) {
JSONPath.cache[scriptCacheKey] = new this.safeVm.Script(script);
} else if (this.currEval === "native") {
JSONPath.cache[scriptCacheKey] = new this.vm.Script(script);
} else if (typeof this.currEval === "function" && this.currEval.prototype && hasOwnProp.call(this.currEval.prototype, "runInNewContext")) {
const CurrEval = this.currEval;
JSONPath.cache[scriptCacheKey] = new CurrEval(script);
} else if (typeof this.currEval === "function") {
JSONPath.cache[scriptCacheKey] = {
runInNewContext: (context) => this.currEval(script, context)
};
} else {
throw new TypeError(`Unknown "eval" property "${this.currEval}"`);
}
}
try {
return JSONPath.cache[scriptCacheKey].runInNewContext(this.currSandbox);
} catch (e) {
if (this.ignoreEvalErrors) {
return false;
}
throw new Error("jsonPath: " + e.message + ": " + code);
}
};
JSONPath.cache = {};
JSONPath.toPathString = function(pathArr) {
const x = pathArr, n = x.length;
let p = "$";
for (let i = 1; i < n; i++) {
if (!/^(~|\^|@.*?\(\))$/u.test(x[i])) {
p += /^[0-9*]+$/u.test(x[i]) ? "[" + x[i] + "]" : "['" + x[i] + "']";
}
}
return p;
};
JSONPath.toPointer = function(pointer) {
const x = pointer, n = x.length;
let p = "";
for (let i = 1; i < n; i++) {
if (!/^(~|\^|@.*?\(\))$/u.test(x[i])) {
p += "/" + x[i].toString().replace(/~/gu, "~0").replace(/\//gu, "~1");
}
}
return p;
};
JSONPath.toPathArray = function(expr) {
const {
cache
} = JSONPath;
if (cache[expr]) {
return cache[expr].concat();
}
const subx = [];
const normalized = expr.replace(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ";$&;").replace(/[['](\??\(.*?\))[\]'](?!.\])/gu, function($0, $1) {
return "[#" + (subx.push($1) - 1) + "]";
}).replace(/\[['"]([^'\]]*)['"]\]/gu, function($0, prop) {
return "['" + prop.replace(/\./gu, "%@%").replace(/~/gu, "%%@@%%") + "']";
}).replace(/~/gu, ";~;").replace(/['"]?\.['"]?(?![^[]*\])|\[['"]?/gu, ";").replace(/%@%/gu, ".").replace(/%%@@%%/gu, "~").replace(/(?:;)?(\^+)(?:;)?/gu, function($0, ups) {
return ";" + ups.split("").join(";") + ";";
}).replace(/;;;|;;/gu, ";..;").replace(/;$|'?\]|'$/gu, "");
const exprList = normalized.split(";").map(function(exp) {
const match = exp.match(/#(\d+)/u);
return !match || !match[1] ? exp : subx[match[1]];
});
cache[expr] = exprList;
return cache[expr].concat();
};
JSONPath.prototype.vm = import_vm.default;
JSONPath.prototype.safeVm = import_vm.default;
var SafeScript = import_vm.default.Script;
// src/index.ts
function pluginHookResponseFilter(_ctx, args) {
const parsed = JSON.parse(args.body);
const filtered = JSONPath({ path: args.filter, json: parsed });
return JSON.stringify(filtered, null, 2);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookResponseFilter
});

View File

@@ -0,0 +1,15 @@
{
"name": "@yaakapp/filter-jsonpath",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"jsonpath-plus": "^9.0.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"name": "@yaakapp/filter-xpath",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"xpath": "^0.0.34"
}
}

View File

@@ -0,0 +1,562 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// ../../node_modules/shell-quote/quote.js
var require_quote = __commonJS({
"../../node_modules/shell-quote/quote.js"(exports2, module2) {
"use strict";
module2.exports = function quote(xs) {
return xs.map(function(s) {
if (s && typeof s === "object") {
return s.op.replace(/(.)/g, "\\$1");
}
if (/["\s]/.test(s) && !/'/.test(s)) {
return "'" + s.replace(/(['\\])/g, "\\$1") + "'";
}
if (/["'\s]/.test(s)) {
return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"';
}
return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
}).join(" ");
};
}
});
// ../../node_modules/shell-quote/parse.js
var require_parse = __commonJS({
"../../node_modules/shell-quote/parse.js"(exports2, module2) {
"use strict";
var CONTROL = "(?:" + [
"\\|\\|",
"\\&\\&",
";;",
"\\|\\&",
"\\<\\(",
"\\<\\<\\<",
">>",
">\\&",
"<\\&",
"[&;()|<>]"
].join("|") + ")";
var controlRE = new RegExp("^" + CONTROL + "$");
var META = "|&;()<> \\t";
var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
var DOUBLE_QUOTE = "'((\\\\'|[^'])*?)'";
var hash = /^#$/;
var SQ = "'";
var DQ = '"';
var DS = "$";
var TOKEN = "";
var mult = 4294967296;
for (i = 0; i < 4; i++) {
TOKEN += (mult * Math.random()).toString(16);
}
var i;
var startsWithToken = new RegExp("^" + TOKEN);
function matchAll(s, r) {
var origIndex = r.lastIndex;
var matches = [];
var matchObj;
while (matchObj = r.exec(s)) {
matches.push(matchObj);
if (r.lastIndex === matchObj.index) {
r.lastIndex += 1;
}
}
r.lastIndex = origIndex;
return matches;
}
function getVar(env, pre, key) {
var r = typeof env === "function" ? env(key) : env[key];
if (typeof r === "undefined" && key != "") {
r = "";
} else if (typeof r === "undefined") {
r = "$";
}
if (typeof r === "object") {
return pre + TOKEN + JSON.stringify(r) + TOKEN;
}
return pre + r;
}
function parseInternal(string, env, opts) {
if (!opts) {
opts = {};
}
var BS = opts.escape || "\\";
var BAREWORD = "(\\" + BS + `['"` + META + `]|[^\\s'"` + META + "])+";
var chunker = new RegExp([
"(" + CONTROL + ")",
// control chars
"(" + BAREWORD + "|" + SINGLE_QUOTE + "|" + DOUBLE_QUOTE + ")+"
].join("|"), "g");
var matches = matchAll(string, chunker);
if (matches.length === 0) {
return [];
}
if (!env) {
env = {};
}
var commented = false;
return matches.map(function(match) {
var s = match[0];
if (!s || commented) {
return void 0;
}
if (controlRE.test(s)) {
return { op: s };
}
var quote = false;
var esc = false;
var out = "";
var isGlob = false;
var i2;
function parseEnvVar() {
i2 += 1;
var varend;
var varname;
var char = s.charAt(i2);
if (char === "{") {
i2 += 1;
if (s.charAt(i2) === "}") {
throw new Error("Bad substitution: " + s.slice(i2 - 2, i2 + 1));
}
varend = s.indexOf("}", i2);
if (varend < 0) {
throw new Error("Bad substitution: " + s.slice(i2));
}
varname = s.slice(i2, varend);
i2 = varend;
} else if (/[*@#?$!_-]/.test(char)) {
varname = char;
i2 += 1;
} else {
var slicedFromI = s.slice(i2);
varend = slicedFromI.match(/[^\w\d_]/);
if (!varend) {
varname = slicedFromI;
i2 = s.length;
} else {
varname = slicedFromI.slice(0, varend.index);
i2 += varend.index - 1;
}
}
return getVar(env, "", varname);
}
for (i2 = 0; i2 < s.length; i2++) {
var c = s.charAt(i2);
isGlob = isGlob || !quote && (c === "*" || c === "?");
if (esc) {
out += c;
esc = false;
} else if (quote) {
if (c === quote) {
quote = false;
} else if (quote == SQ) {
out += c;
} else {
if (c === BS) {
i2 += 1;
c = s.charAt(i2);
if (c === DQ || c === BS || c === DS) {
out += c;
} else {
out += BS + c;
}
} else if (c === DS) {
out += parseEnvVar();
} else {
out += c;
}
}
} else if (c === DQ || c === SQ) {
quote = c;
} else if (controlRE.test(c)) {
return { op: s };
} else if (hash.test(c)) {
commented = true;
var commentObj = { comment: string.slice(match.index + i2 + 1) };
if (out.length) {
return [out, commentObj];
}
return [commentObj];
} else if (c === BS) {
esc = true;
} else if (c === DS) {
out += parseEnvVar();
} else {
out += c;
}
}
if (isGlob) {
return { op: "glob", pattern: out };
}
return out;
}).reduce(function(prev, arg) {
return typeof arg === "undefined" ? prev : prev.concat(arg);
}, []);
}
module2.exports = function parse2(s, env, opts) {
var mapped = parseInternal(s, env, opts);
if (typeof env !== "function") {
return mapped;
}
return mapped.reduce(function(acc, s2) {
if (typeof s2 === "object") {
return acc.concat(s2);
}
var xs = s2.split(RegExp("(" + TOKEN + ".*?" + TOKEN + ")", "g"));
if (xs.length === 1) {
return acc.concat(xs[0]);
}
return acc.concat(xs.filter(Boolean).map(function(x) {
if (startsWithToken.test(x)) {
return JSON.parse(x.split(TOKEN)[1]);
}
return x;
}));
}, []);
};
}
});
// ../../node_modules/shell-quote/index.js
var require_shell_quote = __commonJS({
"../../node_modules/shell-quote/index.js"(exports2) {
"use strict";
exports2.quote = require_quote();
exports2.parse = require_parse();
}
});
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookImport: () => pluginHookImport
});
module.exports = __toCommonJS(src_exports);
var import_shell_quote = __toESM(require_shell_quote());
var DATA_FLAGS = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"];
var SUPPORTED_ARGS = [
["url"],
// Specify the URL explicitly
["user", "u"],
// Authentication
["digest"],
// Apply auth as digest
["header", "H"],
["cookie", "b"],
["get", "G"],
// Put the post data in the URL
["d", "data"],
// Add url encoded data
["data-raw"],
["data-urlencode"],
["data-binary"],
["data-ascii"],
["form", "F"],
// Add multipart data
["request", "X"],
// Request method
DATA_FLAGS
].flatMap((v) => v);
var BOOL_FLAGS = ["G", "get", "digest"];
function pluginHookImport(_ctx, rawData) {
if (!rawData.match(/^\s*curl /)) {
return null;
}
const commands = [];
const normalizedData = rawData.replace(/\ncurl/g, "; curl");
let currentCommand = [];
const parsed = (0, import_shell_quote.parse)(normalizedData);
const normalizedParseEntries = parsed.flatMap((entry) => {
if (typeof entry === "string" && entry.startsWith("-") && !entry.startsWith("--") && entry.length > 2) {
return [entry.slice(0, 2), entry.slice(2)];
}
return entry;
});
for (const parseEntry of normalizedParseEntries) {
if (typeof parseEntry === "string") {
if (parseEntry.startsWith("$")) {
currentCommand.push(parseEntry.slice(1));
} else {
currentCommand.push(parseEntry);
}
continue;
}
if ("comment" in parseEntry) {
continue;
}
const { op } = parseEntry;
if (op === ";") {
commands.push(currentCommand);
currentCommand = [];
continue;
}
if (op?.startsWith("$")) {
const str = op.slice(2, op.length - 1).replace(/\\'/g, "'");
currentCommand.push(str);
continue;
}
if (op === "glob") {
currentCommand.push(parseEntry.pattern);
}
}
commands.push(currentCommand);
const workspace = {
model: "workspace",
id: generateId("workspace"),
name: "Curl Import"
};
const requests = commands.filter((command) => command[0] === "curl").map((v) => importCommand(v, workspace.id));
return {
resources: {
httpRequests: requests,
workspaces: [workspace]
}
};
}
function importCommand(parseEntries, workspaceId) {
const pairsByName = {};
const singletons = [];
for (let i = 1; i < parseEntries.length; i++) {
let parseEntry = parseEntries[i];
if (typeof parseEntry === "string") {
parseEntry = parseEntry.trim();
}
if (typeof parseEntry === "string" && parseEntry.match(/^-{1,2}[\w-]+/)) {
const isSingleDash = parseEntry[0] === "-" && parseEntry[1] !== "-";
let name = parseEntry.replace(/^-{1,2}/, "");
if (!SUPPORTED_ARGS.includes(name)) {
continue;
}
let value;
const nextEntry = parseEntries[i + 1];
const hasValue = !BOOL_FLAGS.includes(name);
if (isSingleDash && name.length > 1) {
value = name.slice(1);
name = name.slice(0, 1);
} else if (typeof nextEntry === "string" && hasValue && !nextEntry.startsWith("-")) {
value = nextEntry;
i++;
} else {
value = true;
}
pairsByName[name] = pairsByName[name] || [];
pairsByName[name].push(value);
} else if (parseEntry) {
singletons.push(parseEntry);
}
}
let urlParameters;
let url;
const urlArg = getPairValue(pairsByName, singletons[0] || "", ["url"]);
const [baseUrl, search] = splitOnce(urlArg, "?");
urlParameters = search?.split("&").map((p) => {
const v = splitOnce(p, "=");
return { name: decodeURIComponent(v[0] ?? ""), value: decodeURIComponent(v[1] ?? ""), enabled: true };
}) ?? [];
url = baseUrl ?? urlArg;
const [username, password] = getPairValue(pairsByName, "", ["u", "user"]).split(/:(.*)$/);
const isDigest = getPairValue(pairsByName, false, ["digest"]);
const authenticationType = username ? isDigest ? "digest" : "basic" : null;
const authentication = username ? {
username: username.trim(),
password: (password ?? "").trim()
} : {};
const headers = [
...pairsByName["header"] || [],
...pairsByName["H"] || []
].map((header) => {
const [name, value] = header.split(/:(.*)$/);
if (!value) {
return {
name: (name ?? "").trim().replace(/;$/, ""),
value: "",
enabled: true
};
}
return {
name: (name ?? "").trim(),
value: value.trim(),
enabled: true
};
});
const cookieHeaderValue = [
...pairsByName["cookie"] || [],
...pairsByName["b"] || []
].map((str) => {
const name = str.split("=", 1)[0];
const value = str.replace(`${name}=`, "");
return `${name}=${value}`;
}).join("; ");
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === "cookie");
if (cookieHeaderValue && existingCookieHeader) {
existingCookieHeader.value += `; ${cookieHeaderValue}`;
} else if (cookieHeaderValue) {
headers.push({
name: "Cookie",
value: cookieHeaderValue,
enabled: true
});
}
const dataParameters = pairsToDataParameters(pairsByName);
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0] : null;
const formDataParams = [
...pairsByName["form"] || [],
...pairsByName["F"] || []
].map((str) => {
const parts = str.split("=");
const name = parts[0] ?? "";
const value = parts[1] ?? "";
const item = {
name,
enabled: true
};
if (value.indexOf("@") === 0) {
item["file"] = value.slice(1);
} else {
item["value"] = value;
}
return item;
});
let body = {};
let bodyType = null;
const bodyAsGET = getPairValue(pairsByName, false, ["G", "get"]);
if (dataParameters.length > 0 && bodyAsGET) {
urlParameters.push(...dataParameters);
} else if (dataParameters.length > 0 && (mimeType == null || mimeType === "application/x-www-form-urlencoded")) {
bodyType = mimeType ?? "application/x-www-form-urlencoded";
body = {
form: dataParameters.map((parameter) => ({
...parameter,
name: decodeURIComponent(parameter.name || ""),
value: decodeURIComponent(parameter.value || "")
}))
};
headers.push({
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: true
});
} else if (dataParameters.length > 0) {
bodyType = mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain" ? mimeType : "other";
body = {
text: dataParameters.map(({ name, value }) => name && value ? `${name}=${value}` : name || value).join("&")
};
} else if (formDataParams.length) {
bodyType = mimeType ?? "multipart/form-data";
body = {
form: formDataParams
};
if (mimeType == null) {
headers.push({
name: "Content-Type",
value: "multipart/form-data",
enabled: true
});
}
}
let method = getPairValue(pairsByName, "", ["X", "request"]).toUpperCase();
if (method === "" && body) {
method = "text" in body || "form" in body ? "POST" : "GET";
}
const request = {
id: generateId("http_request"),
model: "http_request",
workspaceId,
name: "",
urlParameters,
url,
method,
headers,
authentication,
authenticationType,
body,
bodyType,
folderId: null,
sortPriority: 0
};
return request;
}
function pairsToDataParameters(keyedPairs) {
let dataParameters = [];
for (const flagName of DATA_FLAGS) {
const pairs = keyedPairs[flagName];
if (!pairs || pairs.length === 0) {
continue;
}
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
});
}
}
}
return dataParameters;
}
var getPairValue = (pairsByName, defaultValue, names) => {
for (const name of names) {
if (pairsByName[name] && pairsByName[name].length) {
return pairsByName[name][0];
}
}
return defaultValue;
};
function splitOnce(str, sep) {
const index = str.indexOf(sep);
if (index > -1) {
return [str.slice(0, index), str.slice(index + 1)];
}
return [str];
}
var idCount = {};
function generateId(model) {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
});

View File

@@ -0,0 +1,15 @@
{
"name": "@yaakapp/importer-curl",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"shell-quote": "^1.8.1"
},
"devDependencies": {
"@types/shell-quote": "^1.7.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
{
"name": "@yaakapp/importer-insomnia",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"yaml": "^2.4.2"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
{
"name": "@yaakapp/importer-openapi",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"openapi-to-postmanv2": "^4.23.1",
"yaml": "^2.4.2"
},
"devDependencies": {
"@types/openapi-to-postmanv2": "^3.2.4"
}
}

View File

@@ -0,0 +1,301 @@
"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, {
pluginHookImport: () => pluginHookImport
});
module.exports = __toCommonJS(src_exports);
var POSTMAN_2_1_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
var POSTMAN_2_0_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json";
var VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
function pluginHookImport(_ctx, contents) {
const root = parseJSONToRecord(contents);
if (root == null) return;
const info = toRecord(root.info);
const isValidSchema = VALID_SCHEMAS.includes(info.schema);
if (!isValidSchema || !Array.isArray(root.item)) {
return;
}
const globalAuth = importAuth(root.auth);
const exportResources = {
workspaces: [],
environments: [],
httpRequests: [],
folders: []
};
const workspace = {
model: "workspace",
id: generateId("workspace"),
name: info.name || "Postman Import",
description: info.description?.content ?? info.description ?? "",
variables: root.variable?.map((v) => ({
name: v.key,
value: v.value
})) ?? []
};
exportResources.workspaces.push(workspace);
const importItem = (v, folderId = null) => {
if (typeof v.name === "string" && Array.isArray(v.item)) {
const folder = {
model: "folder",
workspaceId: workspace.id,
id: generateId("folder"),
name: v.name,
folderId
};
exportResources.folders.push(folder);
for (const child of v.item) {
importItem(child, folder.id);
}
} else if (typeof v.name === "string" && "request" in v) {
const r = toRecord(v.request);
const bodyPatch = importBody(r.body);
const requestAuthPath = importAuth(r.auth);
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
const headers = toArray(r.header).map((h) => {
return {
name: h.key,
value: h.value,
enabled: !h.disabled
};
});
for (const bodyPatchHeader of bodyPatch.headers) {
const existingHeader = headers.find((h) => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase());
if (existingHeader) {
continue;
}
headers.push(bodyPatchHeader);
}
const { url, urlParameters } = convertUrl(r.url);
const request = {
model: "http_request",
id: generateId("http_request"),
workspaceId: workspace.id,
folderId,
name: v.name,
method: r.method || "GET",
url,
urlParameters,
body: bodyPatch.body,
bodyType: bodyPatch.bodyType,
authentication: authPatch.authentication,
authenticationType: authPatch.authenticationType,
headers
};
exportResources.httpRequests.push(request);
} else {
console.log("Unknown item", v, folderId);
}
};
for (const item of root.item) {
importItem(item);
}
return { resources: convertTemplateSyntax(exportResources) };
}
function convertUrl(url) {
if (typeof url === "string") {
return { url, urlParameters: [] };
}
url = toRecord(url);
let v = "";
if ("protocol" in url && typeof url.protocol === "string") {
v += `${url.protocol}://`;
}
if ("host" in url) {
v += `${Array.isArray(url.host) ? url.host.join(".") : url.host}`;
}
if ("port" in url && typeof url.port === "string") {
v += `:${url.port}`;
}
if ("path" in url && Array.isArray(url.path) && url.path.length > 0) {
v += `/${Array.isArray(url.path) ? url.path.join("/") : url.path}`;
}
const params = [];
if ("query" in url && Array.isArray(url.query) && url.query.length > 0) {
for (const query of url.query) {
params.push({
name: query.key ?? "",
value: query.value ?? "",
enabled: !query.disabled
});
}
}
if ("variable" in url && Array.isArray(url.variable) && url.variable.length > 0) {
for (const v2 of url.variable) {
params.push({
name: ":" + (v2.key ?? ""),
value: v2.value ?? "",
enabled: !v2.disabled
});
}
}
if ("hash" in url && typeof url.hash === "string") {
v += `#${url.hash}`;
}
return { url: v, urlParameters: params };
}
function importAuth(rawAuth) {
const auth = toRecord(rawAuth);
if ("basic" in auth) {
return {
authenticationType: "basic",
authentication: {
username: auth.basic.username || "",
password: auth.basic.password || ""
}
};
} else if ("bearer" in auth) {
return {
authenticationType: "bearer",
authentication: {
token: auth.bearer.token || ""
}
};
} else {
return { authenticationType: null, authentication: {} };
}
}
function importBody(rawBody) {
const body = toRecord(rawBody);
if (body.mode === "graphql") {
return {
headers: [
{
name: "Content-Type",
value: "application/json",
enabled: true
}
],
bodyType: "graphql",
body: {
text: JSON.stringify(
{ query: body.graphql.query, variables: parseJSONToRecord(body.graphql.variables) },
null,
2
)
}
};
} else if (body.mode === "urlencoded") {
return {
headers: [
{
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: true
}
],
bodyType: "application/x-www-form-urlencoded",
body: {
form: toArray(body.urlencoded).map((f) => ({
enabled: !f.disabled,
name: f.key ?? "",
value: f.value ?? ""
}))
}
};
} else if (body.mode === "formdata") {
return {
headers: [
{
name: "Content-Type",
value: "multipart/form-data",
enabled: true
}
],
bodyType: "multipart/form-data",
body: {
form: toArray(body.formdata).map(
(f) => f.src != null ? {
enabled: !f.disabled,
contentType: f.contentType ?? null,
name: f.key ?? "",
file: f.src ?? ""
} : {
enabled: !f.disabled,
name: f.key ?? "",
value: f.value ?? ""
}
)
}
};
} else if (body.mode === "raw") {
return {
headers: [
{
name: "Content-Type",
value: body.options?.raw?.language === "json" ? "application/json" : "",
enabled: true
}
],
bodyType: body.options?.raw?.language === "json" ? "application/json" : "other",
body: {
text: body.raw ?? ""
}
};
} else if (body.mode === "file") {
return {
headers: [],
bodyType: "binary",
body: {
filePath: body.file?.src
}
};
} else {
return { headers: [], bodyType: null, body: {} };
}
}
function parseJSONToRecord(jsonStr) {
try {
return toRecord(JSON.parse(jsonStr));
} catch (err) {
}
return null;
}
function toRecord(value) {
if (Object.prototype.toString.call(value) === "[object Object]") return value;
else return {};
}
function toArray(value) {
if (Object.prototype.toString.call(value) === "[object Array]") return value;
else return [];
}
function convertTemplateSyntax(obj) {
if (typeof obj === "string") {
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}");
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax);
} else if (typeof obj === "object" && obj != null) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)])
);
} else {
return obj;
}
}
var idCount = {};
function generateId(model) {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
});

View File

@@ -0,0 +1,10 @@
{
"name": "@yaakapp/importer-postman",
"private": true,
"version": "0.0.1",
"main": "./build/index.js",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
}
}

View 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, {
pluginHookImport: () => pluginHookImport
});
module.exports = __toCommonJS(src_exports);
function pluginHookImport(_ctx, contents) {
let parsed;
try {
parsed = JSON.parse(contents);
} catch (err) {
return void 0;
}
if (!isJSObject(parsed)) {
return void 0;
}
const isYaakExport = "yaakSchema" in parsed;
if (!isYaakExport) {
return;
}
if ("requests" in parsed.resources) {
parsed.resources.httpRequests = parsed.resources.requests;
delete parsed.resources["requests"];
}
return { resources: parsed.resources };
}
function isJSObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/importer-yaak",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,54 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
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 import_node_fs = __toESM(require("node:fs"));
var plugin = {
templateFunctions: [{
name: "fs.readFile",
args: [{ title: "Select File", type: "file", name: "path", label: "File" }],
async onRender(_ctx, args) {
if (!args.values.path) return null;
try {
return import_node_fs.default.promises.readFile(args.values.path, "utf-8");
} catch (err) {
return null;
}
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-file",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,55 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
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 import_node_fs = __toESM(require("node:fs"));
var plugin = {
templateFunctions: [{
name: "fs.readFile",
description: "Read the contents of a file as utf-8",
args: [{ title: "Select File", type: "file", name: "path", label: "File" }],
async onRender(_ctx, args) {
if (!args.values.path) return null;
try {
return import_node_fs.default.promises.readFile(args.values.path, "utf-8");
} catch (err) {
return null;
}
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-fs",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,49 @@
"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 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");
}
}))
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-hash",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,50 @@
"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: "prompt.text",
description: "Prompt the user for input when sending a request",
args: [
{ type: "text", name: "title", label: "Title" },
{ type: "text", name: "defaultValue", label: "Default Value", optional: true },
{ type: "text", name: "placeholder", label: "Placeholder", optional: true }
],
async onRender(ctx, args) {
if (args.purpose !== "send") return null;
return await ctx.prompt.text({
id: `prompt-${args.values.label}`,
label: args.values.title ?? "",
title: args.values.title ?? "",
defaultValue: args.values.defaultValue,
placeholder: args.values.placeholder
});
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-prompt",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,73 @@
"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: "request.body",
args: [{
name: "requestId",
label: "Http Request",
type: "http_request"
}],
async onRender(ctx, args) {
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? "n/a" });
if (httpRequest == null) return null;
return String(await ctx.templates.render({
data: httpRequest.body?.text ?? "",
purpose: args.purpose
}));
}
},
{
name: "request.header",
args: [
{
name: "requestId",
label: "Http Request",
type: "http_request"
},
{
name: "header",
label: "Header Name",
type: "text"
}
],
async onRender(ctx, args) {
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? "n/a" });
if (httpRequest == null) return null;
const header = httpRequest.headers.find((h) => h.name.toLowerCase() === args.values.header?.toLowerCase());
return String(await ctx.templates.render({
data: header?.value ?? "",
purpose: args.purpose
}));
}
}
]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-request",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"name": "template-function-response",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"jsonpath-plus": "^9.0.0",
"xpath": "^0.0.34",
"@xmldom/xmldom": "^0.8.10"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

@@ -28,7 +28,7 @@ pub struct GrpcConnection {
pub uri: Uri,
}
#[derive(Default)]
#[derive(Default, Debug)]
pub struct StreamError {
pub message: String,
pub status: Option<Status>,
@@ -234,7 +234,7 @@ impl GrpcHandle {
&pool,
input_message,
))
.unwrap(),
.unwrap(),
})
}
def
@@ -301,4 +301,4 @@ fn make_pool_key(id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> String {
);
format!("{:x}", md5::compute(pool_key))
}
}

View File

@@ -153,7 +153,7 @@ async fn file_descriptor_set_from_service_name(
client,
MessageRequest::FileContainingSymbol(service_name.into()),
)
.await
.await
{
Ok(resp) => resp,
Err(e) => {
@@ -249,4 +249,4 @@ pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
.ok_or_else(|| anyhow!("invalid method path"))
.expect("invalid method path");
PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path")
}
}

View File

@@ -16,7 +16,9 @@ export type EnvironmentVariable = { enabled?: boolean, name: string, value: stri
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, 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, trailers: { [key in string]?: string }, url: string, };
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, };
export type GrpcConnectionState = "initialized" | "connected" | "closed";
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
@@ -30,16 +32,22 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, url: string, version: string | null, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
export type KeyValue = { model: "key_value", createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, };
export type ProxySetting = { "type": "enabled", 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, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, proxy: ProxySetting | null, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, variables: Array<EnvironmentVariable>, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -6,6 +6,26 @@ use serde_json::Value;
use std::collections::BTreeMap;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "models.ts")]
pub enum ProxySetting {
Enabled {
http: String,
https: String,
auth: Option<ProxySettingAuth>,
},
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
pub struct ProxySettingAuth {
pub user: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
@@ -27,6 +47,7 @@ pub struct Settings {
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub proxy: Option<ProxySetting>,
}
#[derive(Iden)]
@@ -44,6 +65,7 @@ pub enum SettingsIden {
InterfaceFontSize,
InterfaceScale,
OpenWorkspaceNewWindow,
Proxy,
Telemetry,
Theme,
ThemeDark,
@@ -55,22 +77,24 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
type Error = rusqlite::Error;
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
let proxy: Option<String> = r.get("proxy")?;
Ok(Settings {
id: r.get("id")?,
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
theme: r.get("theme")?,
appearance: r.get("appearance")?,
editor_font_size: r.get("editor_font_size")?,
editor_soft_wrap: r.get("editor_soft_wrap")?,
interface_font_size: r.get("interface_font_size")?,
interface_scale: r.get("interface_scale")?,
open_workspace_new_window: r.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
telemetry: r.get("telemetry")?,
theme: r.get("theme")?,
theme_dark: r.get("theme_dark")?,
theme_light: r.get("theme_light")?,
update_channel: r.get("update_channel")?,
interface_font_size: r.get("interface_font_size")?,
interface_scale: r.get("interface_scale")?,
editor_font_size: r.get("editor_font_size")?,
editor_soft_wrap: r.get("editor_soft_wrap")?,
telemetry: r.get("telemetry")?,
open_workspace_new_window: r.get("open_workspace_new_window")?,
})
}
}
@@ -430,6 +454,21 @@ pub struct HttpResponseHeader {
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
pub enum HttpResponseState {
Initialized,
Connected,
Closed,
}
impl Default for HttpResponseState {
fn default() -> Self {
Self::Initialized
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
@@ -451,6 +490,7 @@ pub struct HttpResponse {
pub remote_addr: Option<String>,
pub status: i32,
pub status_reason: Option<String>,
pub state: HttpResponseState,
pub url: String,
pub version: Option<String>,
}
@@ -475,6 +515,7 @@ pub enum HttpResponseIden {
RemoteAddr,
Status,
StatusReason,
State,
Url,
Version,
}
@@ -484,6 +525,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpResponse {
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
let headers: String = r.get("headers")?;
let state: String = r.get("state")?;
Ok(HttpResponse {
id: r.get("id")?,
model: r.get("model")?,
@@ -500,6 +542,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpResponse {
remote_addr: r.get("remote_addr")?,
status: r.get("status")?,
status_reason: r.get("status_reason")?,
state: serde_json::from_str(format!(r#""{state}""#).as_str()).unwrap(),
body_path: r.get("body_path")?,
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
})
@@ -598,6 +641,21 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
pub enum GrpcConnectionState {
Initialized,
Connected,
Closed,
}
impl Default for GrpcConnectionState {
fn default() -> Self {
Self::Initialized
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
@@ -615,6 +673,7 @@ pub struct GrpcConnection {
pub method: String,
pub service: String,
pub status: i32,
pub state: GrpcConnectionState,
pub trailers: BTreeMap<String, String>,
pub url: String,
}
@@ -634,6 +693,7 @@ pub enum GrpcConnectionIden {
Error,
Method,
Service,
State,
Status,
Trailers,
Url,
@@ -644,6 +704,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
let trailers: String = r.get("trailers")?;
let state: String = r.get("state")?;
Ok(GrpcConnection {
id: r.get("id")?,
model: r.get("model")?,
@@ -654,6 +715,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
service: r.get("service")?,
method: r.get("method")?,
elapsed: r.get("elapsed")?,
state: serde_json::from_str(format!(r#""{state}""#).as_str()).unwrap(),
status: r.get("status")?,
url: r.get("url")?,
error: r.get("error")?,
@@ -873,7 +935,7 @@ impl ModelType {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", untagged)]
#[ts(export, export_to="models.ts")]
#[ts(export, export_to = "models.ts")]
pub enum AnyModel {
CookieJar(CookieJar),
Environment(Environment),

View File

@@ -39,7 +39,6 @@ impl Builder {
create_dir_all(app_path.clone()).expect("Problem creating App directory!");
let db_file_path = app_path.join("db.sqlite");
info!("Opening SQLite DB at {db_file_path:?}");
{
let db_file_path = db_file_path.clone();
@@ -66,7 +65,7 @@ impl Builder {
async fn must_migrate_db<R: Runtime>(app_handle: &AppHandle<R>, path: &PathBuf) {
let app_data_dir = app_handle.path().app_data_dir().unwrap();
let sqlite_file_path = app_data_dir.join("db.sqlite");
info!("Creating database file at {:?}", sqlite_file_path);
File::options()
.write(true)
@@ -76,7 +75,7 @@ async fn must_migrate_db<R: Runtime>(app_handle: &AppHandle<R>, path: &PathBuf)
let p_string = sqlite_file_path.to_string_lossy().replace(' ', "%20");
let url = format!("sqlite://{}?mode=rwc", p_string);
info!("Connecting to database at {}", url);
let opts = SqliteConnectOptions::from_str(path.to_string_lossy().to_string().as_str()).unwrap();
let pool = SqlitePool::connect_with(opts)
@@ -86,11 +85,11 @@ async fn must_migrate_db<R: Runtime>(app_handle: &AppHandle<R>, path: &PathBuf)
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
info!("Running database migrations from: {}", p.to_string_lossy());
let mut m = Migrator::new(p).await.expect("Failed to load migrations");
m.set_ignore_missing(true); // So we can roll back versions and not crash
m.run(&pool).await.expect("Failed to run migrations");
info!("Database migrations complete");
}

View File

@@ -1,10 +1,17 @@
use std::fs;
use crate::error::Result;
use crate::models::{CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden};
use crate::models::{
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest,
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
Settings, SettingsIden, Workspace, WorkspaceIden,
};
use crate::plugin::SqliteConnection;
use log::{debug, error};
use rand::distributions::{Alphanumeric, DistString};
use rusqlite::OptionalExtension;
use sea_query::ColumnRef::Asterisk;
use sea_query::Keyword::CurrentTimestamp;
use sea_query::{Cond, Expr, OnConflict, Order, Query, SqliteQueryBuilder};
@@ -12,6 +19,9 @@ use sea_query_rusqlite::RusqliteBinder;
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
const MAX_GRPC_CONNECTIONS_PER_REQUEST: usize = 20;
const MAX_HTTP_RESPONSES_PER_REQUEST: usize = MAX_GRPC_CONNECTIONS_PER_REQUEST;
pub async fn set_key_value_string<R: Runtime>(
mgr: &WebviewWindow<R>,
namespace: &str,
@@ -108,9 +118,7 @@ pub async fn set_key_value_raw<R: Runtime>(
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db
.prepare(sql.as_str())
.expect("Failed to prepare KeyValue upsert");
let mut stmt = db.prepare(sql.as_str()).expect("Failed to prepare KeyValue upsert");
let kv = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
@@ -134,8 +142,7 @@ pub async fn get_key_value_raw<R: Runtime>(
)
.build_rusqlite(SqliteQueryBuilder);
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into())
.ok()
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
}
pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> {
@@ -356,11 +363,7 @@ pub async fn upsert_grpc_request<R: Runtime>(
request.service.as_ref().map(|s| s.as_str()).into(),
request.method.as_ref().map(|s| s.as_str()).into(),
request.message.as_str().into(),
request
.authentication_type
.as_ref()
.map(|s| s.as_str())
.into(),
request.authentication_type.as_ref().map(|s| s.as_str()).into(),
serde_json::to_string(&request.authentication)?.into(),
serde_json::to_string(&request.metadata)?.into(),
])
@@ -423,6 +426,13 @@ pub async fn upsert_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>,
connection: &GrpcConnection,
) -> Result<GrpcConnection> {
let connections =
list_http_responses_for_request(window, connection.request_id.as_str(), None).await?;
for c in connections.iter().skip(MAX_GRPC_CONNECTIONS_PER_REQUEST - 1) {
debug!("Deleting old grpc connection {}", c.id);
delete_grpc_connection(window, c.id.as_str()).await?;
}
let id = match connection.id.as_str() {
"" => generate_model_id(ModelType::TypeGrpcConnection),
_ => connection.id.to_string(),
@@ -440,6 +450,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
GrpcConnectionIden::Service,
GrpcConnectionIden::Method,
GrpcConnectionIden::Elapsed,
GrpcConnectionIden::State,
GrpcConnectionIden::Status,
GrpcConnectionIden::Error,
GrpcConnectionIden::Trailers,
@@ -454,6 +465,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
connection.service.as_str().into(),
connection.method.as_str().into(),
connection.elapsed.into(),
serde_json::to_value(&connection.state)?.as_str().into(),
connection.status.into(),
connection.error.as_ref().map(|s| s.as_str()).into(),
serde_json::to_string(&connection.trailers)?.into(),
@@ -467,6 +479,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
GrpcConnectionIden::Method,
GrpcConnectionIden::Elapsed,
GrpcConnectionIden::Status,
GrpcConnectionIden::State,
GrpcConnectionIden::Error,
GrpcConnectionIden::Trailers,
GrpcConnectionIden::Url,
@@ -497,6 +510,24 @@ pub async fn get_grpc_connection<R: Runtime>(
}
pub async fn list_grpc_connections<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<GrpcConnection>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(GrpcConnectionIden::Table)
.cond_where(Expr::col(GrpcConnectionIden::WorkspaceId).eq(workspace_id))
.column(Asterisk)
.order_by(GrpcConnectionIden::CreatedAt, Order::Desc)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_grpc_connections_for_request<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
) -> Result<Vec<GrpcConnection>> {
@@ -536,7 +567,7 @@ pub async fn delete_all_grpc_connections<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_grpc_connections(window, request_id).await? {
for r in list_grpc_connections_for_request(window, request_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
@@ -624,7 +655,7 @@ pub async fn list_grpc_events<R: Runtime>(
.from(GrpcEventIden::Table)
.cond_where(Expr::col(GrpcEventIden::ConnectionId).eq(connection_id))
.column(Asterisk)
.order_by(GrpcEventIden::CreatedAt, Order::Desc)
.order_by(GrpcEventIden::CreatedAt, Order::Asc)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
@@ -717,7 +748,7 @@ pub async fn delete_environment<R: Runtime>(
const SETTINGS_ID: &str = "default";
async fn get_settings<R: Runtime>(mgr: &impl Manager<R>) -> Result<Settings> {
async fn get_settings<R: Runtime>(mgr: &impl Manager<R>) -> Result<Option<Settings>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -727,13 +758,15 @@ async fn get_settings<R: Runtime>(mgr: &impl Manager<R>) -> Result<Settings> {
.cond_where(Expr::col(SettingsIden::Id).eq(SETTINGS_ID))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
}
pub async fn get_or_create_settings<R: Runtime>(mgr: &impl Manager<R>) -> Settings {
if let Ok(settings) = get_settings(mgr).await {
return settings;
}
match get_settings(mgr).await {
Ok(Some(settings)) => return settings,
Ok(None) => (),
Err(e) => panic!("Failed to get settings {e:?}"),
};
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -745,11 +778,8 @@ pub async fn get_or_create_settings<R: Runtime>(mgr: &impl Manager<R>) -> Settin
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db
.prepare(sql.as_str())
.expect("Failed to prepare Settings insert");
stmt.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to insert Settings")
let mut stmt = db.prepare(sql.as_str()).expect("Failed to prepare Settings insert");
stmt.query_row(&*params.as_params(), |row| row.try_into()).expect("Failed to insert Settings")
}
pub async fn update_settings<R: Runtime>(
@@ -765,42 +795,23 @@ pub async fn update_settings<R: Runtime>(
.values([
(SettingsIden::Id, "default".into()),
(SettingsIden::CreatedAt, CurrentTimestamp.into()),
(
SettingsIden::Appearance,
settings.appearance.as_str().into(),
),
(SettingsIden::Appearance, settings.appearance.as_str().into()),
(SettingsIden::ThemeDark, settings.theme_dark.as_str().into()),
(SettingsIden::ThemeLight, settings.theme_light.as_str().into()),
(SettingsIden::UpdateChannel, settings.update_channel.into()),
(SettingsIden::InterfaceFontSize, settings.interface_font_size.into()),
(SettingsIden::InterfaceScale, settings.interface_scale.into()),
(SettingsIden::EditorFontSize, settings.editor_font_size.into()),
(SettingsIden::EditorSoftWrap, settings.editor_soft_wrap.into()),
(SettingsIden::Telemetry, settings.telemetry.into()),
(SettingsIden::OpenWorkspaceNewWindow, settings.open_workspace_new_window.into()),
(
SettingsIden::ThemeLight,
settings.theme_light.as_str().into(),
),
(
SettingsIden::UpdateChannel,
settings.update_channel.as_str().into(),
),
(
SettingsIden::InterfaceFontSize,
settings.interface_font_size.into(),
),
(
SettingsIden::InterfaceScale,
settings.interface_scale.into(),
),
(
SettingsIden::EditorFontSize,
settings.editor_font_size.into(),
),
(
SettingsIden::EditorSoftWrap,
settings.editor_soft_wrap.into(),
),
(
SettingsIden::Telemetry,
settings.telemetry.into(),
),
(
SettingsIden::OpenWorkspaceNewWindow,
settings.open_workspace_new_window.into(),
SettingsIden::Proxy,
(match settings.proxy {
None => None,
Some(p) => Some(serde_json::to_string(&p)?),
})
.into(),
),
])
.returning_all()
@@ -872,10 +883,7 @@ pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Res
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_plugin<R: Runtime>(
mgr: &impl Manager<R>,
id: &str
) -> Result<Plugin> {
pub async fn get_plugin<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Plugin> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -888,9 +896,7 @@ pub async fn get_plugin<R: Runtime>(
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_plugins<R: Runtime>(
mgr: &impl Manager<R>,
) -> Result<Vec<Plugin>> {
pub async fn list_plugins<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Plugin>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1185,7 +1191,7 @@ pub async fn delete_http_request<R: Runtime>(
let req = get_http_request(window, id).await?;
// DB deletes will cascade but this will delete the files
delete_all_http_responses(window, id).await?;
delete_all_http_responses_for_request(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1208,6 +1214,7 @@ pub async fn create_default_http_response<R: Runtime>(
0,
0,
"",
HttpResponseState::Initialized,
0,
None,
None,
@@ -1226,6 +1233,7 @@ pub async fn create_http_response<R: Runtime>(
elapsed: i64,
elapsed_headers: i64,
url: &str,
state: HttpResponseState,
status: i64,
status_reason: Option<&str>,
content_length: Option<i64>,
@@ -1234,6 +1242,12 @@ pub async fn create_http_response<R: Runtime>(
version: Option<&str>,
remote_addr: Option<&str>,
) -> Result<HttpResponse> {
let responses = list_http_responses_for_request(window, request_id, None).await?;
for response in responses.iter().skip(MAX_HTTP_RESPONSES_PER_REQUEST - 1) {
debug!("Deleting old response {}", response.id);
delete_http_response(window, response.id.as_str()).await?;
}
let req = get_http_request(window, request_id).await?;
let id = generate_model_id(ModelType::TypeHttpResponse);
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -1250,6 +1264,7 @@ pub async fn create_http_response<R: Runtime>(
HttpResponseIden::Elapsed,
HttpResponseIden::ElapsedHeaders,
HttpResponseIden::Url,
HttpResponseIden::State,
HttpResponseIden::Status,
HttpResponseIden::StatusReason,
HttpResponseIden::ContentLength,
@@ -1267,6 +1282,7 @@ pub async fn create_http_response<R: Runtime>(
elapsed.into(),
elapsed_headers.into(),
url.into(),
serde_json::to_value(state)?.as_str().unwrap_or_default().into(),
status.into(),
status_reason.into(),
content_length.into(),
@@ -1287,10 +1303,11 @@ pub async fn cancel_pending_grpc_connections(app: &AppHandle) -> Result<()> {
let dbm = &*app.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
let (sql, params) = Query::update()
.table(GrpcConnectionIden::Table)
.value(GrpcConnectionIden::Elapsed, -1)
.cond_where(Expr::col(GrpcConnectionIden::Elapsed).eq(0))
.values([(GrpcConnectionIden::State, closed.as_str().into())])
.cond_where(Expr::col(GrpcConnectionIden::State).ne(closed.as_str()))
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params())?;
@@ -1301,13 +1318,14 @@ pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> {
let dbm = &*app.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
let (sql, params) = Query::update()
.table(HttpResponseIden::Table)
.values([
(HttpResponseIden::Elapsed, (-1i32).into()),
(HttpResponseIden::State, closed.as_str().into()),
(HttpResponseIden::StatusReason, "Cancelled".into()),
])
.cond_where(Expr::col(HttpResponseIden::Elapsed).eq(0))
.cond_where(Expr::col(HttpResponseIden::State).ne(closed.as_str()))
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params())?;
@@ -1321,11 +1339,11 @@ pub async fn update_response_if_id<R: Runtime>(
if response.id.is_empty() {
Ok(response.clone())
} else {
update_response(window, response).await
update_http_response(window, response).await
}
}
pub async fn update_response<R: Runtime>(
pub async fn update_http_response<R: Runtime>(
window: &WebviewWindow<R>,
response: &HttpResponse,
) -> Result<HttpResponse> {
@@ -1344,28 +1362,15 @@ pub async fn update_response<R: Runtime>(
HttpResponseIden::StatusReason,
response.status_reason.as_ref().map(|s| s.as_str()).into(),
),
(
HttpResponseIden::ContentLength,
response.content_length.into(),
),
(
HttpResponseIden::BodyPath,
response.body_path.as_ref().map(|s| s.as_str()).into(),
),
(
HttpResponseIden::Error,
response.error.as_ref().map(|s| s.as_str()).into(),
),
(HttpResponseIden::ContentLength, response.content_length.into()),
(HttpResponseIden::BodyPath, response.body_path.as_ref().map(|s| s.as_str()).into()),
(HttpResponseIden::Error, response.error.as_ref().map(|s| s.as_str()).into()),
(
HttpResponseIden::Headers,
serde_json::to_string(&response.headers)
.unwrap_or_default()
.into(),
),
(
HttpResponseIden::Version,
response.version.as_ref().map(|s| s.as_str()).into(),
serde_json::to_string(&response.headers).unwrap_or_default().into(),
),
(HttpResponseIden::Version, response.version.as_ref().map(|s| s.as_str()).into()),
(HttpResponseIden::State, serde_json::to_value(&response.state)?.as_str().into()),
(
HttpResponseIden::RemoteAddr,
response.remote_addr.as_ref().map(|s| s.as_str()).into(),
@@ -1418,17 +1423,37 @@ pub async fn delete_http_response<R: Runtime>(
emit_deleted_model(window, resp)
}
pub async fn delete_all_http_responses<R: Runtime>(
pub async fn delete_all_http_responses_for_request<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_http_responses(window, request_id, None).await? {
for r in list_http_responses_for_request(window, request_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub async fn list_http_responses<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
limit: Option<i64>,
) -> Result<Vec<HttpResponse>> {
let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX);
let dbm = mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(HttpResponseIden::Table)
.cond_where(Expr::col(HttpResponseIden::WorkspaceId).eq(workspace_id))
.column(Asterisk)
.order_by(HttpResponseIden::CreatedAt, Order::Desc)
.limit(limit_unwrapped as u64)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_http_responses_for_request<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
limit: Option<i64>,

View File

@@ -4,9 +4,10 @@ import type { Folder } from "./models";
import type { GrpcRequest } from "./models";
import type { HttpRequest } from "./models";
import type { HttpResponse } from "./models";
import type { JsonValue } from "./serde_json/JsonValue";
import type { Workspace } from "./models";
export type BootRequest = { dir: string, };
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
@@ -32,7 +33,7 @@ export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit: number | null, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
@@ -46,9 +47,9 @@ export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpRequestAction = { key: string, label: string, icon: string | null, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle";
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
@@ -56,9 +57,31 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, windowContext: WindowContext, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "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" } & GetHttpRequestActionsRequest | { "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": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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" };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "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" } & GetHttpRequestActionsRequest | { "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": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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" };
export type OpenFileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
require?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
@@ -70,20 +93,145 @@ export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color | null, icon?: Icon | null, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, args: Array<TemplateFunctionArg>, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg | { "type": "file" } & TemplateFunctionFileArg;
export type TemplateFunctionBaseArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionBaseArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionCheckboxArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionCheckboxArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionHttpRequestArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionFileArg = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<OpenFileFilter>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectArg = { options: Array<TemplateFunctionSelectOption>, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionHttpRequestArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectOption = { name: string, value: string, };
export type TemplateFunctionSelectArg = {
/**
* The options that will be available in the select input
*/
options: Array<TemplateFunctionSelectOption>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionTextArg = { placeholder?: string | null, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
export type TemplateFunctionSelectOption = { label: string, value: string, };
export type TemplateFunctionTextArg = {
/**
* Placeholder for the text input
*/
placeholder?: string,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -14,10 +14,12 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, url: string, version: string | null, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, variables: Array<EnvironmentVariable>, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };

View File

@@ -1,25 +1,40 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tauri::{Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::models::{
Environment, Folder, GrpcRequest, HttpRequest,
HttpResponse, Workspace,
};
use yaak_models::models::{Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, Workspace};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct InternalEvent {
pub id: String,
pub plugin_ref_id: String,
pub reply_id: Option<String>,
pub payload: InternalEventPayload,
pub window_context: WindowContext,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub enum WindowContext {
None,
Label { label: String },
}
impl WindowContext {
pub fn from_window<R: Runtime>(window: &WebviewWindow<R>) -> Self {
Self::Label {
label: window.label().to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")]
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
@@ -56,8 +71,14 @@ pub enum InternalEventPayload {
RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse),
TemplateRenderRequest(TemplateRenderRequest),
TemplateRenderResponse(TemplateRenderResponse),
ShowToastRequest(ShowToastRequest),
PromptTextRequest(PromptTextRequest),
PromptTextResponse(PromptTextResponse),
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
@@ -71,14 +92,15 @@ pub enum InternalEventPayload {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct BootRequest {
pub dir: String,
pub watch: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct BootResponse {
pub name: String,
pub version: String,
@@ -87,21 +109,21 @@ pub struct BootResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct ImportRequest {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct ImportResponse {
pub resources: ImportResources,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct FilterRequest {
pub content: String,
pub filter: String,
@@ -109,49 +131,49 @@ pub struct FilterRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct FilterResponse {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct ExportHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct ExportHttpRequestResponse {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct SendHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CopyTextRequest {
pub text: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest,
pub purpose: RenderPurpose,
@@ -159,25 +181,74 @@ pub struct RenderHttpRequestRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateRenderRequest {
pub data: serde_json::Value,
pub purpose: RenderPurpose,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateRenderResponse {
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct ShowToastRequest {
pub message: String,
#[ts(optional = nullable)]
#[ts(optional)]
pub color: Option<Color>,
#[ts(optional = nullable)]
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct PromptTextRequest {
// A unique ID to identify the prompt (eg. "enter-password")
pub id: String,
// Title to show on the prompt dialog
pub title: String,
// Text to show on the label above the input
pub label: String,
#[ts(optional)]
pub description: Option<String>,
#[ts(optional)]
pub default_value: Option<String>,
#[ts(optional)]
pub placeholder: Option<String>,
/// Text to add to the confirmation button
#[ts(optional)]
pub confirm_text: Option<String>,
/// Text to add to the cancel button
#[ts(optional)]
pub cancel_text: Option<String>,
/// Require the user to enter a non-empty value
#[ts(optional)]
pub require: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct PromptTextResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub enum Color {
Custom,
Default,
@@ -198,17 +269,21 @@ impl Default for Color {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub enum Icon {
Copy,
Info,
CheckCircle,
AlertTriangle,
#[serde(untagged)]
#[ts(type = "\"_unknown\"")]
_Unknown(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct GetTemplateFunctionsResponse {
pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String,
@@ -216,48 +291,65 @@ pub struct GetTemplateFunctionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunction {
pub name: String,
#[ts(optional)]
pub description: Option<String>,
/// Also support alternative names. This is useful for not breaking existing
/// tags when changing the `name` property
#[ts(optional)]
pub aliases: Option<Vec<String>>,
pub args: Vec<TemplateFunctionArg>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub enum TemplateFunctionArg {
Text(TemplateFunctionTextArg),
Select(TemplateFunctionSelectArg),
Checkbox(TemplateFunctionCheckboxArg),
HttpRequest(TemplateFunctionHttpRequestArg),
File(TemplateFunctionFileArg),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionBaseArg {
/// The name of the argument. Should be `camelCase` format
pub name: String,
#[ts(optional = nullable)]
/// Whether the user must fill in the argument
#[ts(optional)]
pub optional: Option<bool>,
#[ts(optional = nullable)]
/// The label of the input
#[ts(optional)]
pub label: Option<String>,
#[ts(optional = nullable)]
/// The default value
#[ts(optional)]
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionTextArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
#[ts(optional = nullable)]
/// Placeholder for the text input
#[ts(optional)]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionHttpRequestArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
@@ -265,16 +357,54 @@ pub struct TemplateFunctionHttpRequestArg {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionFileArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
/// The title of the file selection window
pub title: String,
/// Allow selecting multiple files
#[ts(optional)]
pub multiple: Option<bool>,
// Select a directory, not a file
#[ts(optional)]
pub directory: Option<bool>,
// Default file path for selection dialog
#[ts(optional)]
pub default_path: Option<String>,
// Specify to only allow selection of certain file extensions
#[ts(optional)]
pub filters: Option<Vec<OpenFileFilter>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct OpenFileFilter {
pub name: String,
/// File extensions to require
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionSelectArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
/// The options that will be available in the select input
pub options: Vec<TemplateFunctionSelectOption>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionCheckboxArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
@@ -282,15 +412,15 @@ pub struct TemplateFunctionCheckboxArg {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionSelectOption {
pub name: String,
pub label: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CallTemplateFunctionRequest {
pub name: String,
pub args: CallTemplateFunctionArgs,
@@ -298,14 +428,14 @@ pub struct CallTemplateFunctionRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CallTemplateFunctionResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CallTemplateFunctionArgs {
pub purpose: RenderPurpose,
pub values: HashMap<String, String>,
@@ -313,7 +443,7 @@ pub struct CallTemplateFunctionArgs {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub enum RenderPurpose {
Send,
Preview,
@@ -327,12 +457,12 @@ impl Default for RenderPurpose {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpRequestActionsRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpRequestActionsResponse {
pub actions: Vec<HttpRequestAction>,
pub plugin_ref_id: String,
@@ -340,16 +470,17 @@ pub struct GetHttpRequestActionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct HttpRequestAction {
pub key: String,
pub label: String,
pub icon: Option<String>,
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CallHttpRequestActionRequest {
pub key: String,
pub plugin_ref_id: String,
@@ -358,43 +489,44 @@ pub struct CallHttpRequestActionRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpRequestByIdRequest {
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpRequestByIdResponse {
pub http_request: Option<HttpRequest>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct FindHttpResponsesRequest {
pub request_id: String,
#[ts(optional)]
pub limit: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct FindHttpResponsesResponse {
pub http_responses: Vec<HttpResponse>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to="events.ts")]
#[ts(export, export_to = "events.ts")]
pub struct ImportResources {
pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>,

View File

@@ -5,6 +5,7 @@ use crate::events::{
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
WindowContext,
};
use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle;
@@ -12,11 +13,12 @@ use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntimeServer;
use crate::server::PluginRuntimeServerImpl;
use log::{info, warn};
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
@@ -32,6 +34,12 @@ pub struct PluginManager {
server: Arc<PluginRuntimeServerImpl>,
}
#[derive(Clone)]
struct PluginCandidate {
dir: String,
watch: bool,
}
impl PluginManager {
pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager {
let (events_tx, mut events_rx) = mpsc::channel(128);
@@ -91,7 +99,7 @@ impl PluginManager {
Ok(_) => {
info!("Plugin runtime client connected!");
plugin_manager
.initialize_all_plugins(&app_handle)
.initialize_all_plugins(&app_handle, WindowContext::None)
.await
.expect("Failed to reload plugins");
}
@@ -123,41 +131,65 @@ impl PluginManager {
plugin_manager
}
pub async fn list_plugin_dirs<R: Runtime>(&self, app_handle: &AppHandle<R>) -> Vec<String> {
let plugins_dir = app_handle
async fn list_plugin_dirs<R: Runtime>(
&self,
app_handle: &AppHandle<R>,
) -> Vec<PluginCandidate> {
let bundled_plugins_dir = &app_handle
.path()
.resolve("vendored/plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
let plugins_dir = match env::var("YAAK_PLUGINS_DIR") {
Ok(d) => &PathBuf::from(d),
Err(_) => bundled_plugins_dir,
};
info!("Loading bundled plugins from {plugins_dir:?}");
let bundled_plugin_dirs: Vec<PluginCandidate> = read_plugins_dir(&plugins_dir)
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str())
.iter()
.map(|d| {
let is_vendored = plugins_dir.starts_with(bundled_plugins_dir);
PluginCandidate {
dir: d.into(),
watch: !is_vendored,
}
})
.collect();
let plugins = list_plugins(app_handle).await.unwrap_or_default();
let installed_plugin_dirs = plugins
let installed_plugin_dirs: Vec<PluginCandidate> = plugins
.iter()
.map(|p| p.directory.to_owned())
.collect::<Vec<String>>();
.map(|p| PluginCandidate {
dir: p.directory.to_owned(),
watch: true,
})
.collect();
let plugin_dirs = [bundled_plugin_dirs, installed_plugin_dirs].concat();
plugin_dirs
[bundled_plugin_dirs, installed_plugin_dirs].concat()
}
pub async fn uninstall(&self, dir: &str) -> Result<()> {
pub async fn uninstall(&self, window_context: WindowContext, dir: &str) -> Result<()> {
let plugin = self
.get_plugin_by_dir(dir)
.await
.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(&plugin).await
self.remove_plugin(window_context, &plugin).await
}
async fn remove_plugin(&self, plugin: &PluginHandle) -> Result<()> {
let mut plugins = self.plugins.lock().await;
async fn remove_plugin(
&self,
window_context: WindowContext,
plugin: &PluginHandle,
) -> Result<()> {
// Terminate the plugin
plugin.terminate().await?;
plugin.terminate(window_context).await?;
// Remove the plugin from the list
let mut plugins = self.plugins.lock().await;
let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id);
if let Some(pos) = pos {
plugins.remove(pos);
@@ -166,26 +198,31 @@ impl PluginManager {
Ok(())
}
pub async fn add_plugin_by_dir(&self, dir: &str) -> Result<()> {
pub async fn add_plugin_by_dir(
&self,
window_context: WindowContext,
dir: &str,
watch: bool,
) -> Result<()> {
info!("Adding plugin by dir {dir}");
let maybe_tx = self.server.app_to_plugin_events_tx.lock().await;
let tx = match &*maybe_tx {
None => return Err(ClientNotInitializedErr),
Some(tx) => tx,
};
let ph = PluginHandle::new(dir, tx.clone());
self.plugins.lock().await.push(ph.clone());
let plugin = self
.get_plugin_by_dir(dir)
.await
.ok_or(PluginNotFoundErr(dir.to_string()))?;
let plugin_handle = PluginHandle::new(dir, tx.clone());
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
// Boot the plugin
let event = self
.send_to_plugin_and_wait(
&plugin,
window_context,
&plugin_handle,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
watch,
}),
)
.await?;
@@ -195,7 +232,8 @@ impl PluginManager {
_ => return Err(UnknownEventErr),
};
plugin.set_boot_response(&resp).await;
// Set the boot response
plugin_handle.set_boot_response(&resp).await;
Ok(())
}
@@ -203,19 +241,35 @@ impl PluginManager {
pub async fn initialize_all_plugins<R: Runtime>(
&self,
app_handle: &AppHandle<R>,
window_context: WindowContext,
) -> Result<()> {
for dir in self.list_plugin_dirs(app_handle).await {
let dirs = self.list_plugin_dirs(app_handle).await;
for d in dirs.clone() {
// First remove the plugin if it exists
if let Some(plugin) = self.get_plugin_by_dir(dir.as_str()).await {
if let Err(e) = self.remove_plugin(&plugin).await {
warn!("Failed to remove plugin {dir} {e:?}");
if let Some(plugin) = self.get_plugin_by_dir(d.dir.as_str()).await {
if let Err(e) = self.remove_plugin(window_context.to_owned(), &plugin).await {
warn!("Failed to remove plugin {} {e:?}", d.dir);
}
}
if let Err(e) = self.add_plugin_by_dir(dir.as_str()).await {
warn!("Failed to add plugin {dir} {e:?}");
if let Err(e) = self
.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch)
.await
{
warn!("Failed to add plugin {} {e:?}", d.dir);
}
}
info!(
"Initialized all plugins:\n - {}",
self.plugins
.lock()
.await
.iter()
.map(|p| p.dir.to_string())
.collect::<Vec<String>>()
.join("\n - "),
);
Ok(())
}
@@ -242,12 +296,13 @@ impl PluginManager {
source_event: &InternalEvent,
payload: &InternalEventPayload,
) -> Result<()> {
let reply_id = Some(source_event.clone().id);
let window_label = source_event.to_owned().window_context;
let reply_id = Some(source_event.to_owned().id);
let plugin = self
.get_plugin_by_ref_id(source_event.plugin_ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?;
let event = plugin.build_event_to_send(&payload, reply_id);
let event = plugin.build_event_to_send_raw(window_label, &payload, reply_id);
plugin.send(&event).await
}
@@ -271,7 +326,7 @@ impl PluginManager {
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() {
let info = plugin.info().await?;
let info = plugin.info().await;
if info.name == name {
return Some(plugin);
}
@@ -281,22 +336,29 @@ impl PluginManager {
async fn send_to_plugin_and_wait(
&self,
window_context: WindowContext,
plugin: &PluginHandle,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
let events = self
.send_to_plugins_and_wait(payload, vec![plugin.to_owned()])
.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()])
.await?;
Ok(events.first().unwrap().to_owned())
}
async fn send_and_wait(&self, payload: &InternalEventPayload) -> Result<Vec<InternalEvent>> {
async fn send_and_wait(
&self,
window_context: WindowContext,
payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() };
self.send_to_plugins_and_wait(payload, plugins).await
self.send_to_plugins_and_wait(window_context, payload, plugins)
.await
}
async fn send_to_plugins_and_wait(
&self,
window_context: WindowContext,
payload: &InternalEventPayload,
plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> {
@@ -305,7 +367,7 @@ impl PluginManager {
// 1. Build the events with IDs and everything
let events_to_send = plugins
.iter()
.map(|p| p.build_event_to_send(payload, None))
.map(|p| p.build_event_to_send(window_context.to_owned(), payload, None))
.collect::<Vec<InternalEvent>>();
// 2. Spawn thread to subscribe to incoming events and check reply ids
@@ -350,11 +412,17 @@ impl PluginManager {
Ok(events)
}
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
pub async fn get_http_request_actions<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest(
GetHttpRequestActionsRequest {},
))
.send_and_wait(
WindowContext::from_window(window),
&InternalEventPayload::GetHttpRequestActionsRequest(
GetHttpRequestActionsRequest {},
),
)
.await?;
let mut all_actions = Vec::new();
@@ -367,9 +435,23 @@ impl PluginManager {
Ok(all_actions)
}
pub async fn get_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> {
pub async fn get_template_functions<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
self.get_template_functions_with_context(WindowContext::from_window(window))
.await
}
pub async fn get_template_functions_with_context(
&self,
window_context: WindowContext,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest)
.send_and_wait(
window_context,
&InternalEventPayload::GetTemplateFunctionsRequest,
)
.await?;
let mut all_actions = Vec::new();
@@ -382,13 +464,18 @@ impl PluginManager {
Ok(all_actions)
}
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
pub async fn call_http_request_action<R: Runtime>(
&self,
window: &WebviewWindow<R>,
req: CallHttpRequestActionRequest,
) -> Result<()> {
let ref_id = req.plugin_ref_id.clone();
let plugin = self
.get_plugin_by_ref_id(ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send(
WindowContext::from_window(window),
&InternalEventPayload::CallHttpRequestActionRequest(req),
None,
);
@@ -398,6 +485,7 @@ impl PluginManager {
pub async fn call_template_function(
&self,
window_context: WindowContext,
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
@@ -411,7 +499,10 @@ impl PluginManager {
};
let events = self
.send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req))
.send_and_wait(
window_context,
&InternalEventPayload::CallTemplateFunctionRequest(req),
)
.await?;
let value = events.into_iter().find_map(|e| match e.payload {
@@ -424,11 +515,18 @@ impl PluginManager {
Ok(value)
}
pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> {
pub async fn import_data<R: Runtime>(
&self,
window: &WebviewWindow<R>,
content: &str,
) -> Result<(ImportResponse, String)> {
let reply_events = self
.send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest {
content: content.to_string(),
}))
.send_and_wait(
WindowContext::from_window(window),
&InternalEventPayload::ImportRequest(ImportRequest {
content: content.to_string(),
}),
)
.await?;
// TODO: Don't just return the first valid response
@@ -446,14 +544,14 @@ impl PluginManager {
.get_plugin_by_ref_id(ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(ref_id))?;
let info = plugin.info().await.unwrap();
Ok((resp, info.name))
Ok((resp, plugin.info().await.name))
}
}
}
pub async fn filter_data(
pub async fn filter_data<R: Runtime>(
&self,
window: &WebviewWindow<R>,
filter: &str,
content: &str,
content_type: &str,
@@ -471,6 +569,7 @@ impl PluginManager {
let event = self
.send_to_plugin_and_wait(
WindowContext::from_window(window),
&plugin,
&InternalEventPayload::FilterRequest(FilterRequest {
filter: filter.to_string(),

View File

@@ -26,4 +26,4 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
_ => {}
})
.build()
}
}

View File

@@ -1,9 +1,9 @@
use crate::error::Result;
use crate::events::{BootResponse, InternalEvent, InternalEventPayload};
use crate::events::{BootResponse, InternalEvent, InternalEventPayload, WindowContext};
use crate::server::plugin_runtime::EventStreamEvent;
use crate::util::gen_id;
use std::sync::Arc;
use log::info;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
#[derive(Clone)]
@@ -11,7 +11,7 @@ pub struct PluginHandle {
pub ref_id: String,
pub dir: String,
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<tonic::Result<EventStreamEvent>>>>,
pub(crate) boot_resp: Arc<Mutex<Option<BootResponse>>>,
pub(crate) boot_resp: Arc<Mutex<BootResponse>>,
}
impl PluginHandle {
@@ -22,17 +22,27 @@ impl PluginHandle {
ref_id: ref_id.clone(),
dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)),
boot_resp: Arc::new(Mutex::new(None)),
boot_resp: Arc::new(Mutex::new(BootResponse::default())),
}
}
pub async fn info(&self) -> Option<BootResponse> {
pub async fn info(&self) -> BootResponse {
let resp = &*self.boot_resp.lock().await;
resp.clone()
}
pub fn build_event_to_send(
&self,
window_context: WindowContext,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> InternalEvent {
self.build_event_to_send_raw(window_context, payload, reply_id)
}
pub(crate) fn build_event_to_send_raw(
&self,
window_context: WindowContext,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> InternalEvent {
@@ -41,16 +51,21 @@ impl PluginHandle {
plugin_ref_id: self.ref_id.clone(),
reply_id,
payload: payload.clone(),
window_context,
}
}
pub async fn terminate(&self) -> Result<()> {
pub async fn terminate(&self, window_context: WindowContext) -> Result<()> {
info!("Terminating plugin {}", self.dir);
let event = self.build_event_to_send(&InternalEventPayload::TerminateRequest, None);
let event = self.build_event_to_send(
window_context,
&InternalEventPayload::TerminateRequest,
None,
);
self.send(&event).await
}
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
pub(crate) async fn send(&self, event: &InternalEvent) -> Result<()> {
self.to_plugin_tx
.lock()
.await
@@ -61,17 +76,8 @@ impl PluginHandle {
Ok(())
}
pub async fn send_payload(
&self,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> Result<()> {
let event = self.build_event_to_send(payload, reply_id);
self.send(&event).await
}
pub async fn set_boot_response(&self, resp: &BootResponse) {
let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = Some(resp.clone());
*boot_resp = resp.clone();
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "yaak_sse"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
ts-rs = { version = "10.0.0", features = ["serde-json-impl"] }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ServerSentEvent = { eventType: string, data: string, id: string | null, retry: bigint | null, };

View File

@@ -0,0 +1 @@
export * from './bindings/sse';

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/sse",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1 @@
pub mod sse;

View File

@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "sse.ts")]
pub struct ServerSentEvent {
pub event_type: String,
pub data: String,
pub id: Option<String>,
pub retry: Option<u64>,
}

View File

@@ -50,7 +50,7 @@ async fn render_tag<T: TemplateCallback>(
Some(v) => {
let r = Box::pin(parse_and_render(v, vars, cb)).await;
r.to_string()
},
}
None => "".into(),
},
Val::Bool { value } => value.to_string(),

2
src-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
vite.config.d.ts
vite.config.js

View File

@@ -1,4 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { MotionConfig } from 'framer-motion';
import React, { Suspense } from 'react';
@@ -7,18 +7,25 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const ENABLE_REACT_QUERY_DEVTOOLS = false;
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (err, query) => {
console.log('Query client error', { err, query });
},
}),
defaultOptions: {
queries: {
retry: false,
networkMode: 'always',
refetchOnWindowFocus: true,
networkMode: 'offlineFirst',
refetchOnReconnect: false,
refetchOnMount: false, // Don't refetch when a hook mounts
},
},
});
const ENABLE_REACT_QUERY_DEVTOOLS = false;
export function App() {
return (
<QueryClientProvider client={queryClient}>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { search } from 'fast-fuzzy';
import { fuzzyFilter } from 'fuzzbunny';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
@@ -328,15 +328,21 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const { filteredGroups, filteredAllItems } = useMemo(() => {
const result = command
? search(command, allItems, {
threshold: 0.5,
keySelector: (v) => ('searchText' in v ? v.searchText : v.label),
})
? fuzzyFilter(
allItems.map((i) => ({
...i,
filterBy: 'searchText' in i ? i.searchText : i.label,
})),
command,
{ fields: ['filterBy'] },
).map((v) => v.item)
: allItems;
const filteredGroups = groups
.map((g) => {
g.items = result.filter((i) => g.items.includes(i)).slice(0, MAX_PER_GROUP);
g.items = result
.filter((i) => g.items.find((i2) => i2.key === i.key))
.slice(0, MAX_PER_GROUP);
return g;
})
.filter((g) => g.items.length > 0);

View File

@@ -12,8 +12,8 @@ interface Props {
export const CookieDialog = function ({ cookieJarId }: Props) {
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
const cookieJars = useCookieJars().data ?? [];
const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
const cookieJars = useCookieJars();
const cookieJar = cookieJars?.find((c) => c.id === cookieJarId);
if (cookieJar == null) {
return <div>No cookie jar selected</div>;

View File

@@ -12,7 +12,7 @@ import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext';
export function CookieDropdown() {
const cookieJars = useCookieJars().data ?? [];
const cookieJars = useCookieJars() ?? [];
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
@@ -59,11 +59,12 @@ export function CookieDropdown() {
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
confirmText: 'Save',
placeholder: 'New name',
defaultValue: activeCookieJar?.name,
});
if (name == null) return;
updateCookieJar.mutate({ name });
},
},

View File

@@ -2,8 +2,8 @@ import classNames from 'classnames';
import { Outlet } from 'react-router-dom';
import { useOsInfo } from '../hooks/useOsInfo';
import { DialogProvider, Dialogs } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { ToastProvider, Toasts } from './ToastContext';
import { GlobalHooks } from './GlobalHooks';
export function DefaultLayout() {
const osInfo = useOsInfo();

View File

@@ -6,7 +6,7 @@ import { Dialog } from './core/Dialog';
type DialogEntry = {
id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode;
} & Omit<DialogProps, 'onClose' | 'open' | 'children'>;
} & Omit<DialogProps, 'open' | 'children'>;
interface State {
dialogs: DialogEntry[];
@@ -49,11 +49,18 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
return <DialogContext.Provider value={state}>{children}</DialogContext.Provider>;
};
function DialogInstance({ id, render, ...props }: DialogEntry) {
function DialogInstance({ id, render, onClose, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Dialog open onClose={() => actions.hide(id)} {...props}>
<Dialog
open
onClose={() => {
onClose?.();
actions.hide(id);
}}
{...props}
>
{children}
</Dialog>
);

View File

@@ -274,11 +274,12 @@ function SidebarButton({
Enter a new name for <InlineCode>{environment.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
confirmText: 'Save',
placeholder: 'New Name',
defaultValue: environment.name,
});
if (name == null) return;
updateEnvironment.mutate({ name });
},
},

View File

@@ -1,221 +1,52 @@
import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { useEffect } from 'react';
import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar';
import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { useCopy } from '../hooks/useCopy';
import { environmentsAtom } from '../hooks/useEnvironments';
import { foldersQueryKey } from '../hooks/useFolders';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import {useGenerateThemeCss} from "../hooks/useGenerateThemeCss";
import { useHotKey } from '../hooks/useHotKey';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { pluginsAtom } from '../hooks/usePlugins';
import { usePrompt } from '../hooks/usePrompt';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { settingsAtom, useSettings } from '../hooks/useSettings';
import {useSyncFontSizeSetting} from "../hooks/useSyncFontSizeSetting";
import {useSyncModelStores} from "../hooks/useSyncModelStores";
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import {useSyncZoomSetting} from "../hooks/useSyncZoomSetting";
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { workspacesAtom } from '../hooks/useWorkspaces';
import { useZoom } from '../hooks/useZoom';
import { extractKeyValue } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin';
import { githubLight } from '../lib/theme/themes/github';
import { hotdogStandDefault } from '../lib/theme/themes/hotdog-stand';
import { monokaiProDefault } from '../lib/theme/themes/monokai-pro';
import { rosePineDefault } from '../lib/theme/themes/rose-pine';
import { yaakDark } from '../lib/theme/themes/yaak';
import { getThemeCSS } from '../lib/theme/window';
export interface ModelPayload {
model: AnyModel;
windowLabel: string;
}
export function GlobalHooks() {
useSyncModelStores();
useSyncZoomSetting();
useSyncFontSizeSetting();
useGenerateThemeCss();
// Include here so they always update, even if no component references them
useRecentWorkspaces();
useRecentEnvironments();
useRecentCookieJars();
useRecentRequests();
useSyncWorkspaceChildModels();
// Other useful things
useNotificationToast();
useActiveWorkspaceChangedToast();
useEnsureActiveCookieJar();
// TODO: Remove in future version
useMigrateActiveCookieJarId();
const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette);
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
const { model, windowLabel } = payload;
const queryKey =
model.model === 'http_response'
? httpResponsesQueryKey(model)
: model.model === 'folder'
? foldersQueryKey(model)
: model.model === 'grpc_connection'
? grpcConnectionsQueryKey(model)
: model.model === 'grpc_event'
? grpcEventsQueryKey(model)
: model.model === 'key_value'
? keyValueQueryKey(model)
: model.model === 'cookie_jar'
? cookieJarsQueryKey(model)
: null;
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
wasUpdatedExternally(model.id);
}
const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes(
model.model,
);
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
setGrpcRequests(updateModelList(model, pushToFront));
} else if (model.model === 'environment') {
setEnvironments(updateModelList(model, pushToFront));
} else if (model.model === 'settings') {
setSettings(model);
} else if (queryKey != null) {
// TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => {
if (model.model === 'key_value') {
// Special-case for KeyValue
return extractKeyValue(model);
}
if (Array.isArray(current)) {
return updateModelList(model, pushToFront)(current);
}
});
}
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {
queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model));
} else if (model.model === 'folder') {
queryClient.setQueryData(foldersQueryKey(model), removeById(model));
} else if (model.model === 'environment') {
setEnvironments(removeById(model));
} else if (model.model === 'grpc_request') {
setGrpcRequests(removeById(model));
} else if (model.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(model), removeById(model));
} else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined);
} else if (model.model === 'cookie_jar') {
queryClient.setQueryData(cookieJarsQueryKey(model), undefined);
}
});
const settings = useSettings();
useEffect(() => {
if (settings == null) {
return;
}
const { interfaceScale, editorFontSize } = settings;
getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error);
document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`);
}, [settings]);
// Handle Zoom.
// Note, Mac handles it in the app menu, so need to also handle keyboard
// shortcuts for Windows/Linux
const zoom = useZoom();
useHotKey('app.zoom_in', zoom.zoomIn);
useListenToTauriEvent('zoom_in', zoom.zoomIn);
useHotKey('app.zoom_out', zoom.zoomOut);
useListenToTauriEvent('zoom_out', zoom.zoomOut);
useHotKey('app.zoom_reset', zoom.zoomReset);
useListenToTauriEvent('zoom_reset', zoom.zoomReset);
const copy = useCopy();
useListenToTauriEvent('generate_theme_css', () => {
const themesCss = [
yaakDark,
monokaiProDefault,
rosePineDefault,
catppuccinMacchiato,
githubLight,
hotdogStandDefault,
]
.map(getThemeCSS)
.join('\n\n');
copy(themesCss);
});
const prompt = usePrompt();
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
'show_prompt',
async (event) => {
const value = await prompt(event.payload.args);
const result: PromptTextResponse = { value };
await emit(event.payload.replyId, result);
},
);
return null;
}
function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
return (current: T[]): T[] => {
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
return [...current.slice(0, index), model, ...current.slice(index + 1)];
} else {
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
}
};
}
function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
}
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates
return false;
}
if (payload.model === 'key_value') {
return payload.namespace === 'no_sync';
}
return false;
};

View File

@@ -22,7 +22,7 @@ const emptyArray: string[] = [];
export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateAnyGrpcRequest();
const connections = useGrpcConnections(activeRequest?.id ?? null);
const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest?.id);
const activeConnection = connections[0] ?? null;
const messages = useGrpcEvents(activeConnection?.id ?? null);
const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null);

View File

@@ -188,7 +188,7 @@ function EventRow({
className={classNames(
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
isActive && '!bg-surface-highlight !text',
isActive && '!bg-surface-highlight !text-text',
'text-text-subtle hover:text',
)}
>

View File

@@ -1,4 +1,5 @@
import useResizeObserver from '@react-hook/resize-observer';
import type { GrpcMetadataEntry, GrpcRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
@@ -6,7 +7,6 @@ import { createGlobalState } from 'react-use';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import type { GrpcMetadataEntry, GrpcRequest } from '@yaakapp-internal/models';
import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE } from '../lib/model_util';
import { BasicAuth } from './BasicAuth';
import { BearerAuth } from './BearerAuth';

Some files were not shown because too many files have changed in this diff Show More