Compare commits

...

94 Commits

Author SHA1 Message Date
Gregory Schier
3ecfb15c89 Fix lint 2024-11-16 15:31:14 -08:00
Gregory Schier
23c026126f Tweak schema menu 2024-11-16 15:17:26 -08:00
Gregory Schier
ff9abab547 More control over GraphQL introspection 2024-11-16 14:27:13 -08:00
Gregory Schier
c9c48c77e4 Update Tauri deps 2024-11-16 13:56:32 -08:00
Hao Xiang
83efc58f29 don't lost request's name and folder when updated by curl (#131) 2024-11-14 14:03:22 -08:00
Gregory Schier
632e1ff091 Update README.md 2024-11-12 19:31:55 -08:00
Gregory Schier
40286756b9 Update README.md 2024-11-11 07:48:39 -08:00
Hao Xiang
1050ac5e3c fix(grpc): proto dep topo order to solve panic (#130) 2024-10-29 14:19:11 -07:00
Gregory Schier
6d2c3712c0 Fix active cookie jar and improve routing 2024-10-28 10:06:43 -07:00
Gregory Schier
4a52095033 Better template function fetching 2024-10-24 08:17:58 -07:00
Gregory Schier
55b12d7329 Try fix for template tags not re-fetching on Windows 2024-10-24 07:47:20 -07:00
Gregory Schier
f4240e5229 Prevent bg flash on context menu in sidebar 2024-10-23 10:07:31 -07:00
Gregory Schier
7759649963 Update local model stores in all mutations (#129) 2024-10-23 09:54:43 -07:00
Gregory Schier
c5e6d6f2cb Some tweaks to request deletion 2024-10-23 06:27:38 -07:00
Gregory Schier
ec850f2cf0 Properly handle charset in content-type 2024-10-23 05:49:14 -07:00
Gregory Schier
ff52ad5345 Handle quotes around charset 2024-10-23 05:44:37 -07:00
Gregory Schier
5de50c70c6 Fix workspace/request creation race 2024-10-22 14:27:12 -07:00
Gregory Schier
94f8949ca2 Fix formatter < 0 error 2024-10-22 14:26:45 -07:00
Gregory Schier
44fc3c8d2d Add formatter test for escaped characters 2024-10-22 08:11:03 -07:00
Gregory Schier
57a05d5486 Fix up some of the new formatting stuff 2024-10-22 08:07:56 -07:00
Gregory Schier
e216214085 Custom JSON formatter that works with template syntax (#128) 2024-10-21 15:17:14 -07:00
Gregory Schier
aa7f18a16f Order cmd+k results by match score 2024-10-21 12:54:15 -07:00
Gregory Schier
b9f397e04a Fix response filtering 2024-10-21 07:26:50 -07:00
Gregory Schier
57c3a86799 Animate up instead of down when dropdowns open up 2024-10-18 11:22:05 -07:00
Gregory Schier
52ac41b0c6 Move elapsed calculation 2024-10-18 10:53:04 -07:00
Gregory Schier
741ccbe741 Add labels to plugin event subscribers 2024-10-18 10:46:30 -07:00
Gregory Schier
2ecd86da78 Update README.md 2024-10-18 08:27:24 -07:00
Gregory Schier
30e4e7665a Remove ios config 2024-10-18 07:59:28 -07:00
Gregory Schier
516dfd1f19 Fix GraphQL introspection 2024-10-18 06:57:44 -07:00
Gregory Schier
0cd08499aa Render sending gRPC events 2024-10-17 12:03:35 -07:00
Gregory Schier
c652df82a3 Fix SSE event selection 2024-10-17 11:28:10 -07:00
Gregory Schier
c8342fb0a9 Delete send history for workspace 2024-10-17 11:17:27 -07:00
Gregory Schier
d0b59a0fb4 Show folder structure in request selection 2024-10-17 10:53:48 -07:00
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
Gregory Schier
974425afee Fix JSONPath/XPath filtering 2024-09-26 06:45:03 -07:00
Gregory Schier
9f7f06a142 Don't adhere to wrapLines in single-line inputs 2024-09-25 22:16:35 -07:00
229 changed files with 183294 additions and 12366 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

@@ -1,4 +1,4 @@
# [Yaak API Client](https://yaak.app)
# Yaak API Client
Yaak is a desktop API client for organizing and executing REST, GraphQL, and gRPC
requests. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
@@ -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.

2636
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.1.0",
"@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

1166
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.3", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
@@ -29,8 +29,8 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
yaak_plugin_runtime = { workspace = true }
yaak_sse = { workspace = true }
yaak_models = { workspace = true }
anyhow = "1.0.86"
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
@@ -41,27 +41,31 @@ rand = "0.8.5"
regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
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.3"
tauri-plugin-fs = "2.0.3"
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_sse = { path = "yaak_sse" }
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"] }
serde = "1.0.215"
serde_json = "1.0.132"
tauri-plugin-shell = "2.0.2"
tauri = { version = "2.1.1", features = ["devtools", "protocol-asset"] }
thiserror = "2.0.3"
ts-rs = "10.0.0"

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

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -84,7 +84,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
@@ -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

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -84,7 +84,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
@@ -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(uuid::Uuid::new_v4().to_string())
} else {
base_dir.join(response_id.clone())
};
{
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)) => {
let mut r = response.lock().await;
r.elapsed = start.elapsed().as_millis() as i32;
f.write_all(&bytes).await.expect("Failed to write to file");
f.flush().await.expect("Failed to flush file");
written_bytes += bytes.len();
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",
);
}
}

File diff suppressed because it is too large Load Diff

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

@@ -72,10 +72,8 @@
"rpm"
],
"createUpdaterArtifacts": "v1Compatible",
"iOS": {
"developmentTeam": "7PU3P6ELJ8"
},
"macOS": {
"minimumSystemVersion": "13.0",
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []

View File

@@ -0,0 +1,109 @@
"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?.query === "string") {
const body = { query: request.body.query || "", variables: maybeParseJSON(request.body.variables, void 0) };
xs.push("--data-raw", `${quote(JSON.stringify(body))}`);
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;
}
function maybeParseJSON(v, fallback) {
try {
return JSON.parse(v);
} catch (err) {
return fallback;
}
}
// 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

@@ -22,3 +22,4 @@ tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
md5 = "0.7.0"
dunce = "1.0.4"
async-recursion = "1.1.1"

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

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use std::str::FromStr;
use anyhow::anyhow;
use async_recursion::async_recursion;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
@@ -38,9 +39,8 @@ pub async fn fill_pool_from_files(
.expect("failed to resolve protoc include directory");
// HACK: Remove UNC prefix for Windows paths
let global_import_dir = dunce::simplified(global_import_dir.as_path())
.to_string_lossy()
.to_string();
let global_import_dir =
dunce::simplified(global_import_dir.as_path()).to_string_lossy().to_string();
let desc_path = dunce::simplified(desc_path.as_path());
let mut args = vec![
@@ -89,12 +89,9 @@ pub async fn fill_pool_from_files(
let bytes = fs::read(desc_path).await.map_err(|e| e.to_string())?;
let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?;
pool.add_file_descriptor_set(fdp)
.map_err(|e| e.to_string())?;
pool.add_file_descriptor_set(fdp).map_err(|e| e.to_string())?;
fs::remove_file(desc_path)
.await
.map_err(|e| e.to_string())?;
fs::remove_file(desc_path).await.map_err(|e| e.to_string())?;
Ok(pool)
}
@@ -107,6 +104,10 @@ pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, Stri
if service == "grpc.reflection.v1alpha.ServerReflection" {
continue;
}
if service == "grpc.reflection.v1.ServerReflection"{
// TODO: update reflection client to use v1
continue;
}
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
}
@@ -120,10 +121,7 @@ pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
http_connector.enforce_http(false);
http_connector
});
Client::builder()
.pool_max_idle_per_host(0)
.http2_only(true)
.build(connector)
Client::builder().pool_max_idle_per_host(0).http2_only(true).build(connector)
}
async fn list_services(
@@ -137,11 +135,7 @@ async fn list_services(
_ => panic!("Expected a ListServicesResponse variant"),
};
Ok(list_services_response
.service
.iter()
.map(|s| s.name.clone())
.collect::<Vec<_>>())
Ok(list_services_response.service.iter().map(|s| s.name.clone()).collect::<Vec<_>>())
}
async fn file_descriptor_set_from_service_name(
@@ -157,10 +151,7 @@ async fn file_descriptor_set_from_service_name(
{
Ok(resp) => resp,
Err(e) => {
warn!(
"Error fetching file descriptor for service {}: {}",
service_name, e
);
warn!("Error fetching file descriptor for service {}: {}", service_name, e);
return;
}
};
@@ -170,16 +161,37 @@ async fn file_descriptor_set_from_service_name(
_ => panic!("Expected a FileDescriptorResponse variant"),
};
for fd in file_descriptor_response.file_descriptor_proto {
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
.await;
}
#[async_recursion]
async fn add_file_descriptors_to_pool(
fds: Vec<Vec<u8>>,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) {
let mut topo_sort = topology::SimpleTopoSort::new();
let mut fd_mapping = std::collections::HashMap::with_capacity(fds.len());
for fd in fds {
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
// Add deps first or else we'll get an error
for dep_name in fdp.clone().dependency {
file_descriptor_set_by_filename(&dep_name, pool, client).await;
}
topo_sort.insert(fdp.name().to_string(), fdp.dependency.clone());
fd_mapping.insert(fdp.name().to_string(), fdp);
}
pool.add_file_descriptor_proto(fdp)
.expect("add file descriptor proto");
for node in topo_sort {
match node {
Ok(node) => {
if let Some(fdp) = fd_mapping.remove(&node) {
pool.add_file_descriptor_proto(fdp).expect("add file descriptor proto");
} else {
file_descriptor_set_by_filename(node.as_str(), pool, client).await;
}
}
Err(_) => panic!("proto file got cycle!"),
}
}
}
@@ -206,11 +218,8 @@ async fn file_descriptor_set_by_filename(
}
};
for fd in file_descriptor_response.file_descriptor_proto {
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
pool.add_file_descriptor_proto(fdp)
.expect("add file descriptor proto");
}
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
.await;
}
async fn send_reflection_request(
@@ -250,3 +259,136 @@ pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
.expect("invalid method path");
PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path")
}
mod topology {
use std::collections::{HashMap, HashSet};
pub struct SimpleTopoSort<T> {
out_graph: HashMap<T, HashSet<T>>,
in_graph: HashMap<T, HashSet<T>>,
}
impl<T> SimpleTopoSort<T>
where
T: Eq + std::hash::Hash + Clone,
{
pub fn new() -> Self {
SimpleTopoSort {
out_graph: HashMap::new(),
in_graph: HashMap::new(),
}
}
pub fn insert<I: IntoIterator<Item = T>>(&mut self, node: T, deps: I) {
self.out_graph.entry(node.clone()).or_insert(HashSet::new());
for dep in deps {
self.out_graph.entry(node.clone()).or_insert(HashSet::new()).insert(dep.clone());
self.in_graph.entry(dep.clone()).or_insert(HashSet::new()).insert(node.clone());
}
}
}
impl<T> IntoIterator for SimpleTopoSort<T>
where
T: Eq + std::hash::Hash + Clone,
{
type IntoIter = SimpleTopoSortIter<T>;
type Item = <SimpleTopoSortIter<T> as Iterator>::Item;
fn into_iter(self) -> Self::IntoIter {
SimpleTopoSortIter::new(self)
}
}
pub struct SimpleTopoSortIter<T> {
data: SimpleTopoSort<T>,
zero_indegree: Vec<T>,
}
impl<T> SimpleTopoSortIter<T>
where
T: Eq + std::hash::Hash + Clone,
{
pub fn new(data: SimpleTopoSort<T>) -> Self {
let mut zero_indegree = Vec::new();
for (node, _) in data.in_graph.iter() {
if !data.out_graph.contains_key(node) {
zero_indegree.push(node.clone());
}
}
for (node, deps) in data.out_graph.iter(){
if deps.is_empty(){
zero_indegree.push(node.clone());
}
}
SimpleTopoSortIter {
data,
zero_indegree,
}
}
}
impl<T> Iterator for SimpleTopoSortIter<T>
where
T: Eq + std::hash::Hash + Clone,
{
type Item = Result<T, &'static str>;
fn next(&mut self) -> Option<Self::Item> {
if self.zero_indegree.is_empty() {
if self.data.out_graph.is_empty() {
return None;
}
return Some(Err("Cycle detected"));
}
let node = self.zero_indegree.pop().unwrap();
if let Some(parents) = self.data.in_graph.get(&node){
for parent in parents.iter(){
let deps = self.data.out_graph.get_mut(parent).unwrap();
deps.remove(&node);
if deps.is_empty() {
self.zero_indegree.push(parent.clone());
}
}
}
self.data.out_graph.remove(&node);
Some(Ok(node))
}
}
#[test]
fn test_sort(){
{
let mut topo_sort = SimpleTopoSort::new();
topo_sort.insert("a", []);
for node in topo_sort {
match node {
Ok(n) => assert_eq!(n, "a"),
Err(e) => panic!("err {}", e),
}
}
}
{
let mut topo_sort = SimpleTopoSort::new();
topo_sort.insert("a", ["b"]);
topo_sort.insert("b", []);
let mut iter = topo_sort.into_iter();
match iter.next() {
Some(Ok(n)) => assert_eq!(n, "b"),
_ => panic!("err"),
}
match iter.next() {
Some(Ok(n)) => assert_eq!(n, "a"),
_ => panic!("err"),
}
assert_eq!(iter.next(), None);
}
}
}

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,8 +6,10 @@ pub enum Error {
SqlError(#[from] rusqlite::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Model not found {0}")]
ModelNotFound(String),
#[error("unknown error")]
Unknown,
}
pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T> = std::result::Result<T, Error>;

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,18 @@
use std::fs;
use crate::error::Error::ModelNotFound;
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 +20,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 +119,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 +143,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>> {
@@ -292,7 +300,12 @@ pub async fn duplicate_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let mut request = get_grpc_request(window, id).await?.clone();
let mut request = match get_grpc_request(window, id).await? {
Some(r) => r,
None => {
return Err(ModelNotFound(id.to_string()));
}
};
request.id = "".to_string();
upsert_grpc_request(window, &request).await
}
@@ -301,7 +314,12 @@ pub async fn delete_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let req = get_grpc_request(window, id).await?;
let req = match get_grpc_request(window, id).await? {
Some(r) => r,
None => {
return Err(ModelNotFound(id.to_string()));
}
};
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -356,11 +374,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(),
])
@@ -390,7 +404,10 @@ pub async fn upsert_grpc_request<R: Runtime>(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcRequest> {
pub async fn get_grpc_request<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<Option<GrpcRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -400,7 +417,7 @@ pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
.cond_where(Expr::col(GrpcRequestIden::Id).eq(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 list_grpc_requests<R: Runtime>(
@@ -423,6 +440,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 +464,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 +479,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 +493,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
GrpcConnectionIden::Method,
GrpcConnectionIden::Elapsed,
GrpcConnectionIden::Status,
GrpcConnectionIden::State,
GrpcConnectionIden::Error,
GrpcConnectionIden::Trailers,
GrpcConnectionIden::Url,
@@ -496,7 +523,25 @@ pub async fn get_grpc_connection<R: Runtime>(
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_connections<R: Runtime>(
pub async fn list_grpc_connections_for_workspace<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 +581,17 @@ 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(())
}
pub async fn delete_all_grpc_connections_for_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
for r in list_grpc_connections_for_workspace(window, workspace_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
@@ -624,7 +679,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 +772,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 +782,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 +802,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 +819,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 +907,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 +920,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();
@@ -1067,7 +1097,10 @@ pub async fn duplicate_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let mut request = get_http_request(window, id).await?.clone();
let mut request = match get_http_request(window, id).await? {
None => return Err(ModelNotFound(id.to_string())),
Some(r) => r,
};
request.id = "".to_string();
upsert_http_request(window, request).await
}
@@ -1165,7 +1198,10 @@ pub async fn list_http_requests<R: Runtime>(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<HttpRequest> {
pub async fn get_http_request<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<Option<HttpRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1175,17 +1211,20 @@ pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
.cond_where(Expr::col(HttpRequestIden::Id).eq(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 delete_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let req = get_http_request(window, id).await?;
let req = match get_http_request(window, id).await? {
None => return Err(ModelNotFound(id.to_string())),
Some(r) => r,
};
// 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 +1247,7 @@ pub async fn create_default_http_response<R: Runtime>(
0,
0,
"",
HttpResponseState::Initialized,
0,
None,
None,
@@ -1226,6 +1266,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,7 +1275,16 @@ pub async fn create_http_response<R: Runtime>(
version: Option<&str>,
remote_addr: Option<&str>,
) -> Result<HttpResponse> {
let req = get_http_request(window, request_id).await?;
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 = match get_http_request(window, request_id).await? {
None => return Err(ModelNotFound(request_id.to_string())),
Some(r) => r,
};
let id = generate_model_id(ModelType::TypeHttpResponse);
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1250,6 +1300,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 +1318,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 +1339,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 +1354,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 +1375,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 +1398,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 +1459,47 @@ 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>(
pub async fn delete_all_http_responses_for_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
for r in list_http_responses_for_workspace(window, workspace_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub async fn list_http_responses_for_workspace<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,25 +241,41 @@ 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(())
}
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
pub async fn subscribe(&self, label: &str) -> (String, mpsc::Receiver<InternalEvent>) {
let (tx, rx) = mpsc::channel(128);
let rx_id = generate_id();
let rx_id = format!("{label}_{}", generate_id());
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
}
@@ -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,31 +336,39 @@ 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>> {
let (rx_id, mut rx) = self.subscribe().await;
let label = format!("wait[{}]", plugins.len());
let (rx_id, mut rx) = self.subscribe(label.as_str()).await;
// 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 +413,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 +436,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 +465,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 +486,7 @@ impl PluginManager {
pub async fn call_template_function(
&self,
window_context: WindowContext,
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
@@ -411,7 +500,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 +516,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,31 +545,32 @@ 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,
) -> Result<FilterResponse> {
let plugin_name = if content_type.to_lowercase().contains("json") {
"filter-jsonpath"
"@yaakapp/filter-jsonpath"
} else {
"filter-xpath"
"@yaakapp/filter-xpath"
};
let plugin = self
.get_plugin_by_dir(plugin_name)
.get_plugin_by_name(plugin_name)
.await
.ok_or(PluginNotFoundErr(plugin_name.to_string()))?;
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

@@ -0,0 +1,304 @@
enum FormatState {
TemplateTag,
String,
None,
}
/// Formats JSON that might contain template tags (skipped entirely)
pub fn format_json(text: &str, tab: &str) -> String {
let mut chars = text.chars().peekable();
let mut new_json = "".to_string();
let mut depth = 0;
let mut state = FormatState::None;
loop {
let rest_of_chars = chars.clone();
let current_char = match chars.next() {
None => break,
Some(c) => c,
};
// Handle JSON string states
if let FormatState::String = state {
match current_char {
'"' => {
state = FormatState::None;
new_json.push(current_char);
continue;
}
'\\' => {
new_json.push(current_char);
if let Some(c) = chars.next() {
new_json.push(c);
}
continue;
}
_ => {
new_json.push(current_char);
continue;
}
}
}
// Close Template tag states
if let FormatState::TemplateTag = state {
if rest_of_chars.take(2).collect::<String>() == "]}" {
state = FormatState::None;
new_json.push_str("]}");
chars.next(); // Skip the second closing bracket
continue;
} else {
new_json.push(current_char);
continue;
}
}
if rest_of_chars.take(3).collect::<String>() == "${[" {
state = FormatState::TemplateTag;
new_json.push_str("${[");
chars.next(); // Skip {
chars.next(); // Skip [
continue;
}
match current_char {
',' => {
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
'{' => match chars.peek() {
Some('}') => {
new_json.push(current_char);
new_json.push('}');
chars.next(); // Skip }
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'[' => match chars.peek() {
Some(']') => {
new_json.push(current_char);
new_json.push(']');
chars.next(); // Skip ]
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'}' => {
// Guard just in case invalid JSON has more closes than opens
if depth > 0 {
depth -= 1;
}
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
}
']' => {
// Guard just in case invalid JSON has more closes than opens
if depth > 0 {
depth -= 1;
}
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
}
':' => {
new_json.push(current_char);
new_json.push(' '); // Pad with space
}
'"' => {
state = FormatState::String;
new_json.push(current_char);
}
_ => {
if current_char == ' '
|| current_char == '\n'
|| current_char == '\t'
|| current_char == '\r'
{
// Don't add these
} else {
new_json.push(current_char);
}
}
}
}
// Replace only lines containing whitespace with nothing
new_json
.lines()
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
.join("\n") // Join the lines back into a single string
}
#[cfg(test)]
mod test {
use crate::format::format_json;
#[test]
fn test_simple_object() {
assert_eq!(
format_json(r#"{"foo":"bar","baz":"qux"}"#, " "),
r#"
{
"foo": "bar",
"baz": "qux"
}
"#
.trim()
);
}
#[test]
fn test_escaped() {
assert_eq!(
format_json(r#"{"foo":"Hi \"world!\""}"#, " "),
r#"
{
"foo": "Hi \"world!\""
}
"#
.trim()
);
}
#[test]
fn test_simple_array() {
assert_eq!(
format_json(r#"["foo","bar","baz","qux"]"#, " "),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_extra_whitespace() {
assert_eq!(
format_json(
r#"["foo", "bar", "baz","qux"
]"#,
" "
),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_invalid_json() {
assert_eq!(
format_json(r#"["foo", {"bar", }"baz",["qux" ]]"#, " "),
r#"
[
"foo",
{
"bar",
}"baz",
[
"qux"
]
]
"#
.trim()
);
}
#[test]
fn test_skip_template_tags() {
assert_eq!(
format_json(r#"{"foo":${[ fn("hello", "world") ]} }"#, " "),
r#"
{
"foo": ${[ fn("hello", "world") ]}
}
"#
.trim()
);
}
#[test]
fn test_graphql_response() {
assert_eq!(
format_json(
r#"{"data":{"capsules":[{"landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null},{"id":"5e9e2c5bf3591882af3b2665","landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null}]}}"#,
" "
),
r#"
{
"data": {
"capsules": [
{
"landings": null,
"original_launch": null,
"reuse_count": 0,
"status": "retired",
"type": "Dragon 1.0",
"missions": null
},
{
"id": "5e9e2c5bf3591882af3b2665",
"landings": null,
"original_launch": null,
"reuse_count": 0,
"status": "retired",
"type": "Dragon 1.0",
"missions": null
}
]
}
}
"#
.trim()
);
}
#[test]
fn test_immediate_close() {
assert_eq!(
format_json(r#"{"bar":[]}"#, " "),
r#"
{
"bar": []
}
"#
.trim()
);
}
#[test]
fn test_more_closes() {
assert_eq!(
format_json(r#"{}}"#, " "),
r#"
{}
}
"#
.trim()
);
}
}

View File

@@ -1,5 +1,6 @@
pub mod parser;
pub mod renderer;
pub mod format;
pub use parser::*;
pub use renderer::*;

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,6 +1,6 @@
import { lazy } from 'react';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { paths, useAppRoutes } from '../hooks/useAppRoutes';
import { DefaultLayout } from './DefaultLayout';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError';
@@ -19,19 +19,23 @@ const router = createBrowserRouter([
element: <RedirectToLatestWorkspace />,
},
{
path: routePaths.workspaces(),
path: paths.workspaces(),
element: <RedirectToLatestWorkspace />,
},
{
path: routePaths.workspace({
path: paths.workspace({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
{
path: routePaths.request({
path: paths.request({
workspaceId: ':workspaceId',
requestId: ':requestId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
@@ -40,8 +44,10 @@ const router = createBrowserRouter([
element: <RedirectLegacyEnvironmentURLs />,
},
{
path: routePaths.workspaceSettings({
path: paths.workspaceSettings({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
element: <LazySettings />,
},
@@ -64,13 +70,13 @@ function RedirectLegacyEnvironmentURLs() {
workspaceId?: string;
environmentId?: string;
}>();
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
const environmentId = (rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId) ?? null;
let to;
if (workspaceId != null && requestId != null) {
to = routes.paths.request({ workspaceId, environmentId, requestId });
to = routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId: null });
} else if (workspaceId != null) {
to = routes.paths.workspace({ workspaceId, environmentId });
to = routes.paths.workspace({ workspaceId, environmentId, cookieJarId: null });
} else {
to = routes.paths.workspaces();
}

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { search } from 'fast-fuzzy';
import type { KeyboardEvent, ReactNode } from 'react';
import type { HotkeyAction } from '../hooks/useHotKey';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
@@ -13,7 +14,6 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useOpenSettings } from '../hooks/useOpenSettings';
@@ -271,7 +271,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return routes.navigate('request', {
workspaceId: r.workspaceId,
requestId: r.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
},
});
@@ -313,7 +314,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
workspaceCommands,
sortedRequests,
routes,
activeEnvironment,
activeEnvironment?.id,
activeCookieJar?.id,
sortedEnvironments,
setActiveEnvironmentId,
sortedWorkspaces,
@@ -328,15 +330,23 @@ 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'] },
)
.sort((a, b) => b.score - a.score)
.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>
);

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