Compare commits

..

54 Commits

Author SHA1 Message Date
Gregory Schier
736025b12f Move editor search to top 2025-03-19 08:22:57 -07:00
Gregory Schier
cb9e9a67a3 Try registering URL scheme 2025-03-19 07:58:12 -07:00
Gregory Schier
93c323458f Tweak Git history table 2025-03-19 06:59:54 -07:00
Gregory Schier
6f8c03d8c1 Fix git confid for commit 2025-03-19 06:59:43 -07:00
Gregory Schier
afd4228fcf Don't style scrollbars on mac 2025-03-19 06:49:14 -07:00
Gregory Schier
d478e5a12e Hotkey scrolling 2025-03-19 06:48:29 -07:00
Gregory Schier
0db9ebe67d Better Codemirror search match styles 2025-03-19 06:48:07 -07:00
Gregory Schier
80ea5e6b91 Fix autoscroller header scrolling 2025-03-19 06:37:02 -07:00
Gregory Schier
cb773babe1 Nested template functions (#186) 2025-03-18 12:49:19 -07:00
Gregory Schier
b9ed554aca Remove useTemplating prop (#184) 2025-03-18 05:34:38 -07:00
Gregory Schier
f42f3d0e27 Support multi-line params and env vars 2025-03-17 09:29:37 -07:00
Gregory Schier
93ba5b6e5c Fix close bracket bug 2025-03-13 13:09:13 -07:00
Gregory Schier
be11d5968e Fix notification not showing all 2025-03-12 06:41:53 -07:00
Gregory Schier
0828599e4f Don't switch to XML for HTML responses.
Fixes https://feedback.yaak.app/p/issue-with-rendering-html-responses-after-update
2025-03-08 08:34:41 -08:00
dependabot[bot]
f47d22c395 Bump ring from 0.17.8 to 0.17.13 in /src-tauri (#181)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 08:11:38 -08:00
Gregory Schier
12233cb6f6 Build plugins 2025-03-08 08:06:18 -08:00
Hao Xiang
cdce2ac53a fix ws connection state (#175)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-03-08 08:03:16 -08:00
Gregory Schier
f4d0371060 Merge remote-tracking branch 'origin/master' 2025-03-06 07:15:07 -08:00
Gregory Schier
787a0433cb Support _PREFIXED variable names and fail when variable missing 2025-03-06 07:15:02 -08:00
Gregory Schier
60ea408e51 Add sponsor button
Some people want to contribute additional funds, so this makes it easier.
2025-03-06 06:46:02 -08:00
Gregory Schier
0db0cdfd6c Only font rendering fix for Linux 2025-03-06 06:29:03 -08:00
Gregory Schier
26371e5f6b Ignore whitespace during content type detection 2025-03-06 06:22:21 -08:00
Hermes Junior
6b7c144a11 Fix font aliasing on webkit. (#178) 2025-03-06 06:19:16 -08:00
Andy Bao
62f43ca24c Fix wrong protoc includes path (#179) 2025-03-06 06:18:06 -08:00
Gregory Schier
fbf4d3c11e Make rendering return Result, and handle infinite recursion 2025-03-05 13:49:45 -08:00
Gregory Schier
7a1a0689b0 Add ability to deactivate license 2025-03-05 07:13:19 -08:00
Hao Xiang
9ead45d67a fix plugin manager listen addr (#177) 2025-03-02 05:51:19 -08:00
Gregory Schier
eb8153f409 Better trial activation flows 2025-02-25 22:16:55 -08:00
Gregory Schier
80de232bec Fix dropdown button icon 2025-02-25 19:52:57 -08:00
Gregory Schier
7af8c95fea Allow opening workspace if sync dir not empty 2025-02-25 06:54:30 -08:00
Gregory Schier
2db72fe6ef Fix WS duplication from context menu 2025-02-25 06:10:35 -08:00
Gregory Schier
d297e92a5a Fix content type parsing exception 2025-02-24 22:44:58 -08:00
Gregory Schier
7e1da4395d Build OAuth 2 plugin 2025-02-24 22:34:29 -08:00
Gregory Schier
7f8b0479e1 Plugin window data directory key 2025-02-24 22:32:40 -08:00
Gregory Schier
c8d6183456 Reduce plugin runtime memory 2025-02-24 12:20:47 -08:00
Gregory Schier
9d5f7784c4 Fix code splitting from tanstack/router migration 2025-02-24 07:12:45 -08:00
Gregory Schier
05ac836265 Remove analytics and add more update headers 2025-02-24 06:31:49 -08:00
Gregory Schier
af7782c93b Better license flows 2025-02-24 05:59:15 -08:00
Gregory Schier
2b1431d041 Merge remote-tracking branch 'origin/master' 2025-02-23 06:25:59 -08:00
Gregory Schier
9d8b7a5265 Tweak getting content type 2025-02-23 06:25:53 -08:00
dependabot[bot]
95c12ad291 Bump openssl from 0.10.66 to 0.10.70 in /src-tauri (#161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-23 06:09:26 -08:00
Gregory Schier
dac2cec52f Merge remote-tracking branch 'origin/master' 2025-02-23 06:06:43 -08:00
Gregory Schier
efe4eef1b7 Fix deleting selected environment 2025-02-23 06:05:01 -08:00
Hao Xiang
a0e196a9e7 adding alternate key combinations for special shift (#173) 2025-02-22 07:00:04 -08:00
Gregory Schier
c6427dc724 Update README.md 2025-02-21 14:02:29 -08:00
Gregory Schier
8ce1e22b4e Update README.md 2025-02-21 13:57:51 -08:00
Gregory Schier
022d725e03 Update issue templates 2025-02-21 13:53:54 -08:00
Gregory Schier
ed7fdb1b4c Only close brackets for json-like langs
Fixes #162
2025-02-21 13:50:21 -08:00
Gregory Schier
e510204b8c More monospace fallbacks
Closes #167
2025-02-21 13:36:54 -08:00
Gregory Schier
d31b4448df Pass templating vars to recursive children (closes #171) 2025-02-21 13:34:15 -08:00
Gregory Schier
e420a0a45e Don't expand the directory setting when creating workspace 2025-02-21 13:18:47 -08:00
Gregory Schier
84ecbe0cd6 Better querystring import (https://feedback.yaak.app/p/url-pasted-params-parsed-incorrectly) 2025-02-21 13:16:09 -08:00
Gregory Schier
6a63cc26b9 Fix commit-and-push loading state 2025-02-19 10:35:41 -08:00
Gregory Schier
8ed0fd55c3 Remove environments from synced folder, and stop syncing 2025-02-19 10:35:31 -08:00
195 changed files with 4033 additions and 2870 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: gschier
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: https://yaak.app/pricing

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -60,3 +60,10 @@ 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._
## Lezer Grammer Generation
```sh
# Example
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
```

View File

@@ -7,25 +7,24 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
## Feature Overview
🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
🔎 Filter response bodies using JSONPath or XPath queries.<br/>
⛓️ Chain together multiple requests to dynamically reference values.<br/>
📂 Organize requests into workspaces and nested folders.<br/>
🧮 Use environment variables to easily switch between Prod and Dev.<br/>
🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
🎨 Choose from many of the included themes, or make your own.<br/>
💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
📜 View response history for each request.<br/>
🔌 Create your own plugins for authentication, template tags, and more!<br/>
🛜 Configure a proxy to access firewall-blocked APIs
- 🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
- 📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
- 🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
- 🔎 Filter response bodies using JSONPath or XPath queries.<br/>
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
- 📂 Organize requests into workspaces and nested folders.<br/>
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
- 🎨 Choose from many of the included themes, or make your own.<br/>
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
- 📜 View response history for each request.<br/>
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
- 🛜 Configure a proxy to access firewall-blocked APIs
## Feedback and Bug Reports
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.
[feedback.yaak.app](https://feedback.yaak.app).
## Community Projects
@@ -34,9 +33,5 @@ in this repository if applicable.
## Contribution Policy
Yaak is 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.
Yaak is open source, but only accepting contributions for bug fixes. To get started,
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.

199
package-lock.json generated
View File

@@ -2500,9 +2500,9 @@
}
},
"node_modules/@tanstack/history": {
"version": "1.95.0",
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.95.0.tgz",
"integrity": "sha512-w1/yWuIBqmG0Z0MPMf1OuOCce7FXyVH4L4dIA4rvpnjIUCH8qRUgloFAVg37nTMUbOmhMsY2NZDxCpKBv+CLJg==",
"version": "1.99.13",
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.99.13.tgz",
"integrity": "sha512-JMd7USmnp8zV8BRGIjALqzPxazvKtQ7PGXQC7n39HpbqdsmfV2ePCzieO84IvN+mwsTrXErpbjI4BfKCa+ZNCg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -2513,9 +2513,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.62.16",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.16.tgz",
"integrity": "sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g==",
"version": "5.66.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz",
"integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2534,12 +2534,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.62.16",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.16.tgz",
"integrity": "sha512-XJIZNj65d2IdvU8VBESmrPakfIm6FSdHDzrS1dPrAwmq3ZX+9riMh/ZfbNQHAWnhrgmq7KoXpgZSRyXnqMYT9A==",
"version": "5.66.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz",
"integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.62.16"
"@tanstack/query-core": "5.66.4"
},
"funding": {
"type": "github",
@@ -2568,14 +2568,15 @@
}
},
"node_modules/@tanstack/react-router": {
"version": "1.95.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.95.1.tgz",
"integrity": "sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==",
"version": "1.111.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.111.3.tgz",
"integrity": "sha512-OsqAuExa4WF7+BbjENWlb4dHRousxU5jahJHUPyO0gaUcWwzaVloJKi8lTFTd1PWQ8waz5V7BedkV67hd8syUw==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.95.0",
"@tanstack/history": "1.99.13",
"@tanstack/react-store": "^0.7.0",
"jsesc": "^3.0.2",
"@tanstack/router-core": "^1.111.3",
"jsesc": "^3.1.0",
"tiny-invariant": "^1.3.3",
"tiny-warning": "^1.0.3"
},
@@ -2587,8 +2588,8 @@
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
"react": ">=18.0.0 || >=19.0.0",
"react-dom": ">=18.0.0 || >=19.0.0"
}
},
"node_modules/@tanstack/react-store": {
@@ -2610,12 +2611,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
"integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.11.2"
"@tanstack/virtual-core": "3.13.0"
},
"funding": {
"type": "github",
@@ -2626,6 +2627,23 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/router-core": {
"version": "1.111.3",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.111.3.tgz",
"integrity": "sha512-q+CHuOhTgqHudVKijL89jIdLe5A00RzV8ZMMSi4qiHGnggm4nisF8eSE3dFQaic1+YFk1wR7dfFA2hvkr1hFIA==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.99.13",
"@tanstack/store": "^0.7.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/router-devtools": {
"version": "1.91.3",
"resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.91.3.tgz",
@@ -2730,9 +2748,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
"integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2754,9 +2772,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.2.tgz",
"integrity": "sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.3.0.tgz",
"integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -2963,63 +2981,63 @@
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.2.2.tgz",
"integrity": "sha512-bZvDLMqfcNmsw7Ag8I49jlaCjdpDvvlJHnpp6P+Gg/3xtpSERdwlDxm7cKGbs2mj46dsw4AuG3RoAgcpwgioUA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0.tgz",
"integrity": "sha512-ApNkejXP2jpPBSifznPPcHTXxu9/YaRW+eJ+8+nYwqp0lLUtebFHG4QhxitM43wwReHE81WAV1DQ/b+2VBftOA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.0.tgz",
"integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0.tgz",
"integrity": "sha512-BNEeQQ5aH8J5SwYuWgRszVyItsmquRuzK2QRkVj8Z0sCsLnSvJFYI3JHRzzr3ltZGq1nMPtblrlZzuKqVzRawA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.0.tgz",
"integrity": "sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0.tgz",
"integrity": "sha512-C+NII9vzswqnOQE8k7oRtnaw0z5TZsMmnirRhXkCKDEhQQH9841Us/PC1WHtGiAaJ8za1A1JB2xXndEq/47X/w==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.2.tgz",
"integrity": "sha512-E/XIHKqGV+FT8PDdkfMETmgPUxcR79Rk8USuzbadD/ZdvsKCfQR5q+6rpZC9zEnG2wzi9lVQM4D3xwrtGGIB8A==",
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-os": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.2.1.tgz",
"integrity": "sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz",
"integrity": "sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz",
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
@@ -6963,17 +6981,19 @@
}
},
"node_modules/framer-motion": {
"version": "11.11.7",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.7.tgz",
"integrity": "sha512-89CgILOXPeG3L7ymOTGrLmf8IiKubYLUN/QkYgQuLvehAHfqgwJbLfCnhuyRI4WTds1TXkUp67A7IJrgRY/j1w==",
"version": "12.4.7",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz",
"integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.4.5",
"motion-utils": "^12.0.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
@@ -8577,9 +8597,9 @@
}
},
"node_modules/jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -10008,6 +10028,47 @@
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"license": "BSD-3-Clause"
},
"node_modules/motion": {
"version": "12.4.7",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz",
"integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.4.7",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.4.5",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz",
"integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.0.0"
}
},
"node_modules/motion-utils": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -15496,7 +15557,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.4.1",
"version": "0.5.0",
"dependencies": {
"@types/node": "^22.5.4"
},
@@ -15635,17 +15696,17 @@
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.62.16",
"@tanstack/react-router": "^1.95.1",
"@tanstack/react-virtual": "^3.11.2",
"@tauri-apps/api": "^2.0.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
"@tauri-apps/plugin-log": "^2.0.0",
"@tauri-apps/plugin-opener": "^2.2.2",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@tanstack/react-query": "^5.66.9",
"@tanstack/react-router": "^1.111.3",
"@tanstack/react-virtual": "^3.13.0",
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-log": "^2.3.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"cm6-graphql": "^0.0.9",
@@ -15655,7 +15716,6 @@
"eventemitter3": "^5.0.1",
"focus-trap-react": "^10.2.3",
"format-graphql": "^1.5.0",
"framer-motion": "^11.5.4",
"fuzzbunny": "^1.0.1",
"hexy": "^0.3.5",
"history": "^5.3.0",
@@ -15663,6 +15723,7 @@
"js-md5": "^0.8.3",
"lucide-react": "^0.474.0",
"mime": "^4.0.4",
"motion": "^12.4.7",
"nanoid": "^5.0.9",
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.4.1",
"version": "0.5.0",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -346,7 +346,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
@@ -354,7 +354,7 @@ export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**

View File

@@ -32,7 +32,10 @@ export interface Context {
};
window: {
openUrl(
args: OpenWindowRequest & { onNavigate?: (args: { url: string }) => void },
args: OpenWindowRequest & {
onNavigate?: (args: { url: string }) => void;
onClose: () => void;
},
): Promise<{ close: () => void }>;
};
httpRequest: {

View File

@@ -3,10 +3,7 @@
"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",
"build:__main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.cjs",
"build:__worker": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.worker.cjs"
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs"
},
"dependencies": {
"ws": "^8.18.0"

View File

@@ -1,14 +1,19 @@
import type { InternalEvent } from "@yaakapp/api";
import EventEmitter from "node:events";
import type { InternalEvent } from '@yaakapp/api';
export class EventChannel {
emitter: EventEmitter = new EventEmitter();
#listeners = new Set<(event: InternalEvent) => void>();
emit(e: InternalEvent) {
this.emitter.emit("__plugin_event__", e);
for (const l of this.#listeners) {
l(e);
}
}
listen(cb: (e: InternalEvent) => void) {
this.emitter.on("__plugin_event__", cb);
this.#listeners.add(cb);
}
unlisten(cb: (e: InternalEvent) => void) {
this.#listeners.delete(cb);
}
}

View File

@@ -1,56 +1,28 @@
import type { BootRequest, InternalEvent } from '@yaakapp/api';
import path from 'node:path';
import { Worker } from 'node:worker_threads';
import type { EventChannel } from './EventChannel';
import type { PluginWorkerData } from './index.worker';
import { PluginInstance, PluginWorkerData } from './PluginInstance';
export class PluginHandle {
#worker: Worker;
#instance: PluginInstance;
constructor(
readonly pluginRefId: string,
readonly bootRequest: BootRequest,
readonly events: EventChannel,
readonly pluginToAppEvents: EventChannel,
) {
this.#worker = this.#createWorker();
}
sendToWorker(event: InternalEvent) {
this.#worker.postMessage(event);
}
async terminate() {
await this.#worker.terminate();
}
#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,
});
worker.on('message', (e) => this.events.emit(e));
worker.on('error', this.#handleError.bind(this));
worker.on('exit', this.#handleExit.bind(this));
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
console.log('Created plugin worker for ', this.bootRequest.dir);
return worker;
}
async #handleError(err: Error) {
console.error('Plugin errored', this.bootRequest.dir, err);
sendToWorker(event: InternalEvent) {
this.#instance.postMessage(event);
}
async #handleExit(code: number) {
if (code === 0) {
console.log('Plugin exited successfully', this.bootRequest.dir);
} else {
console.log('Plugin exited with status', code, this.bootRequest.dir);
}
terminate() {
this.#instance.terminate();
}
}

View File

@@ -0,0 +1,597 @@
import type {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
WindowContext,
} from '@yaakapp/api';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
// import util from 'node:util';
import { EventChannel } from './EventChannel';
// import { interceptStdout } from './interceptStdout';
import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
}
export class PluginInstance {
#workerData: PluginWorkerData;
#mod: PluginDefinition;
#pkg: { name?: string; version?: string };
#pluginToAppEvents: EventChannel;
#appToPluginEvents: EventChannel;
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
this.#workerData = workerData;
this.#pluginToAppEvents = pluginEvents;
this.#appToPluginEvents = new EventChannel();
// Forward incoming events to onMessage()
this.#appToPluginEvents.listen(async event => {
await this.#onMessage(event);
})
// Reload plugin if the JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const fileChangeCallback = async () => {
this.#importModule();
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
if (this.#workerData.bootRequest.watch) {
watchFile(this.#pathMod(), fileChangeCallback);
watchFile(this.#pathPkg(), fileChangeCallback);
}
this.#mod = {};
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
// TODO: Re-implement this now that we're not using workers
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
this.#importModule();
}
postMessage(event: InternalEvent) {
this.#appToPluginEvents.emit(event);
}
terminate() {
this.#unimportModule();
}
async #onMessage(event: InternalEvent) {
const ctx = this.#newCtx(event);
const { windowContext, payload, id: replyId } = event;
try {
if (payload.type === 'boot_request') {
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
const payload: InternalEventPayload = {
type: 'boot_response',
name: this.#pkg.name ?? 'unknown',
version: this.#pkg.version ?? '0.0.1',
};
this.#sendPayload(windowContext, payload, replyId);
return;
}
if (payload.type === 'terminate_request') {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
this.#sendPayload(windowContext, payload, replyId);
return;
}
if (
payload.type === 'import_request' &&
typeof this.#mod?.importer?.onImport === 'function'
) {
const reply = await this.#mod.importer.onImport(ctx, {
text: payload.content,
});
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
// deno-lint-ignore no-explicit-any
resources: reply.resources as any,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
}
}
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') {
const reply = await this.#mod.filter.onFilter(ctx, {
filter: payload.filter,
payload: payload.content,
mimeType: payload.type,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply.filtered,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(this.#mod?.httpRequestActions)
) {
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId: this.#workerData.pluginRefId,
functions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
const { name, shortLabel, label } = this.#mod.authentication;
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response',
name,
label,
shortLabel,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
resolvedArgs.push(v);
}
}
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
}
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response',
args: resolvedArgs,
actions: resolvedActions,
pluginRefId: this.#workerData.pluginRefId,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
this.#sendPayload(
windowContext,
{
type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
},
replyId,
);
return;
}
}
if (
payload.type === 'call_http_authentication_action_request' &&
this.#mod.authentication != null
) {
const action = this.#mod.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(this.#mod.httpRequestActions)
) {
const action = this.#mod.httpRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const action = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') {
applyFormInputDefaults(action.args, payload.args.values);
const result = await action.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
return;
}
}
if (payload.type === 'reload_request') {
this.#importModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
this.#sendPayload(
windowContext,
{
type: 'error_response',
error: `${err}`,
},
replyId,
);
return;
}
// No matches, so send back an empty response so the caller doesn't block forever
this.#sendEmpty(windowContext, replyId);
}
#pathMod() {
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js');
}
#pathPkg() {
return path.join(this.#workerData.bootRequest.dir, 'package.json');
}
#unimportModule() {
const id = require.resolve(this.#pathMod());
delete require.cache[id];
}
#importModule() {
const id = require.resolve(this.#pathMod());
delete require.cache[id];
this.#mod = require(id).plugin;
}
#buildEventToSend(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return {
pluginRefId: this.#workerData.pluginRefId,
pluginName: path.basename(this.#workerData.bootRequest.dir),
id: genId(),
replyId,
payload,
windowContext,
};
}
#sendPayload(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = this.#buildEventToSend(windowContext, payload, replyId);
this.#sendEvent(event);
return event.id;
}
#sendEvent(event: InternalEvent) {
// if (event.payload.type !== 'empty_response') {
// console.log('Sending event to app', this.#pkg.name, event.id, event.payload.type);
// }
this.#pluginToAppEvents.emit(event);
}
#sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: WindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<T>((resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
this.#appToPluginEvents.unlisten(cb); // Unlisten, now that we're done
const { type: _, ...payload } = event.payload;
resolve(payload as T);
}
};
this.#appToPluginEvents.listen(cb);
});
// 3. Send the event after we start listening (to prevent race)
this.#sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
#sendAndListenForEvents(
windowContext: WindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
// 2. Listen for replies in the background
this.#appToPluginEvents.listen((event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
onEvent(event.payload);
}
});
// 3. Send the event after we start listening (to prevent race)
this.#sendEvent(eventToSend);
}
#newCtx(event: InternalEvent): Context {
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'copy_text_request',
text,
});
},
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'show_toast_request',
...args,
});
},
},
window: {
openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') {
onNavigate?.(event);
} else if (event.type === 'window_close_event') {
onClose?.();
}
};
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
this.#sendPayload(event.windowContext, closePayload, null);
},
};
},
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
type: 'prompt_text_request',
...args,
});
return reply.value;
},
},
httpResponse: {
find: async (args) => {
const payload = {
type: 'find_http_responses_request',
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
event.windowContext,
payload,
);
return httpResponses;
},
},
httpRequest: {
getById: async (args) => {
const payload = {
type: 'get_http_request_by_id_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
send: async (args) => {
const payload = {
type: 'send_http_request_request',
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
payload,
);
return httpResponse;
},
render: async (args) => {
const payload = {
type: 'render_http_request_request',
...args,
} as const;
const { httpRequest } = await this.#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.
*/
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
event.windowContext,
payload,
);
return result.data;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
event.windowContext,
payload,
);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
set: async <T>(key: string, value: T) => {
const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = {
type: 'set_key_value_request',
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
payload,
);
return result.deleted;
},
},
};
}
}
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';
for (let i = 0; i < len; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return id;
}
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: FormInput[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}
const watchedFiles: Record<string, Stats> = {};
/**
* Watch a file and trigger callback on change.
*
* We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes
*/
function watchFile(filepath: string, cb: (filepath: string) => void) {
watch(filepath, () => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
}
watchedFiles[filepath] = stat;
});
}
// function prefixStdout(s: string) {
// if (!s.includes('%s')) {
// throw new Error('Console prefix must contain a "%s" replacer');
// }
// interceptStdout((text: string) => {
// const lines = text.split(/\n/);
// let newText = '';
// for (let i = 0; i < lines.length; i++) {
// if (lines[i] == '') continue;
// newText += util.format(s, lines[i]) + '\n';
// }
// return newText.trimEnd();
// });
// }

View File

@@ -8,7 +8,7 @@ if (!port) {
throw new Error('Plugin runtime missing PORT')
}
const events = new EventChannel();
const pluginToAppEvents = new EventChannel();
const plugins: Record<string, PluginHandle> = {};
const ws = new WebSocket(`ws://localhost:${port}`);
@@ -25,7 +25,7 @@ ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
// Listen for incoming events from plugins
events.listen((e) => {
pluginToAppEvents.listen((e) => {
const eventStr = JSON.stringify(e);
ws.send(eventStr);
});
@@ -34,7 +34,7 @@ async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, events);
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents);
plugins[pluginEvent.pluginRefId] = plugin;
}
@@ -46,7 +46,7 @@ async function handleIncoming(msg: string) {
}
if (pluginEvent.payload.type === 'terminate_request') {
await plugin.terminate();
plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}

View File

@@ -1,568 +0,0 @@
// OAuth 2.0 spec -> https://datatracker.ietf.org/doc/html/rfc6749
import type {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
WindowContext,
} from '@yaakapp/api';
import * as console from 'node:console';
import type { Stats } from 'node:fs';
import { readFileSync, statSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
import { parentPort as nullableParentPort, workerData } from 'node:worker_threads';
import { interceptStdout } from './interceptStdout';
import { migrateTemplateFunctionSelectOptions } from './migrations';
if (nullableParentPort == null) {
throw new Error('Worker does not have access to parentPort');
}
const parentPort = nullableParentPort;
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
}
function initialize(workerData: PluginWorkerData) {
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');
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
prefixStdout(`[plugin][${pkg.name}] %s`);
function buildEventToSend(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return {
pluginRefId,
pluginName: path.basename(pluginDir),
id: genId(),
replyId,
payload,
windowContext,
};
}
function sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
return sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
function sendPayload(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = buildEventToSend(windowContext, payload, replyId);
sendEvent(event);
return event.id;
}
function sendEvent(event: InternalEvent) {
if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type);
}
parentPort.postMessage(event);
}
function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: WindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<T>((resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
parentPort.off('message', cb); // Unlisten, now that we're done
const { type: _, ...payload } = event.payload;
resolve(payload as T);
}
};
parentPort.on('message', cb);
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
function sendAndListenForEvents(
windowContext: WindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Listen for replies in the background
parentPort.on('message', (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
onEvent(event.payload);
}
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
}
// Reload plugin if the JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const fileChangeCallback = async () => {
importModule();
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
if (enableWatch) {
watchFile(pathMod, fileChangeCallback);
watchFile(pathPkg, fileChangeCallback);
}
const newCtx = (event: InternalEvent): Context => ({
clipboard: {
async copyText(text) {
await sendAndWaitForReply(event.windowContext, {
type: 'copy_text_request',
text,
});
},
},
toast: {
async show(args) {
await sendAndWaitForReply(event.windowContext, {
type: 'show_toast_request',
...args,
});
},
},
window: {
async openUrl({ onNavigate, ...args }) {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') {
onNavigate?.(event);
}
};
sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
sendPayload(event.windowContext, closePayload, null);
},
};
},
},
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>(
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>(
event.windowContext,
payload,
);
return httpRequest;
},
async send(args) {
const payload = {
type: 'send_http_request_request',
...args,
} as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
payload,
);
return httpResponse;
},
async render(args) {
const payload = {
type: 'render_http_request_request',
...args,
} as const;
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;
},
},
store: {
async get<T>(key: string) {
const payload = { type: 'get_key_value_request', key } as const;
const result = await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
async set<T>(key: string, value: T) {
const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = {
type: 'set_key_value_request',
key,
value: valueStr,
};
await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
async delete(key: string) {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
payload,
);
return result.deleted;
},
},
});
let plug: PluginDefinition | null = null;
function importModule() {
const id = require.resolve(pathMod);
delete require.cache[id];
plug = require(id).plugin;
}
importModule();
// Message comes into the plugin to be processed
parentPort.on('message', async (event: InternalEvent) => {
const ctx = newCtx(event);
const { windowContext, payload, id: replyId } = event;
try {
if (payload.type === 'boot_request') {
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
const payload: InternalEventPayload = {
type: 'boot_response',
name: pkg.name,
version: pkg.version,
};
sendPayload(windowContext, payload, replyId);
return;
}
if (payload.type === 'terminate_request') {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
sendPayload(windowContext, payload, replyId);
return;
}
if (payload.type === 'import_request' && typeof plug?.importer?.onImport === 'function') {
const reply = await plug.importer.onImport(ctx, {
text: payload.content,
});
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
// deno-lint-ignore no-explicit-any
resources: reply.resources as any,
};
sendPayload(windowContext, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
}
}
if (payload.type === 'filter_request' && typeof plug?.filter?.onFilter === 'function') {
const reply = await plug.filter.onFilter(ctx, {
filter: payload.filter,
payload: payload.content,
mimeType: payload.type,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply.filtered,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(plug?.httpRequestActions)
) {
const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId,
actions: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(plug?.templateFunctions)
) {
const reply: TemplateFunction[] = plug.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId,
functions: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_summary_request' && plug?.authentication) {
const { name, shortLabel, label } = plug.authentication;
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response',
name,
label,
shortLabel,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_config_request' && plug?.authentication) {
const { args, actions } = plug.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
resolvedArgs.push(v);
}
}
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
}
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response',
args: resolvedArgs,
actions: resolvedActions,
pluginRefId,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && plug?.authentication) {
const auth = plug.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
sendPayload(
windowContext,
{
type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
},
replyId,
);
return;
}
}
if (
payload.type === 'call_http_authentication_action_request' &&
plug?.authentication != null
) {
const action = plug.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(plug?.httpRequestActions)
) {
const action = plug.httpRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(plug?.templateFunctions)
) {
const action = plug.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') {
applyFormInputDefaults(action.args, payload.args.values);
const result = await action.onRender(ctx, payload.args);
sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
return;
}
}
if (payload.type === 'reload_request') {
importModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
sendPayload(
windowContext,
{
type: 'error_response',
error: `${err}`,
},
replyId,
);
return;
}
// No matches, so send back an empty response so the caller doesn't block forever
sendEmpty(windowContext, replyId);
});
}
initialize(workerData);
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';
for (let i = 0; i < len; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return id;
}
function prefixStdout(s: string) {
if (!s.includes('%s')) {
throw new Error('Console prefix must contain a "%s" replacer');
}
interceptStdout((text: string) => {
const lines = text.split(/\n/);
let newText = '';
for (let i = 0; i < lines.length; i++) {
if (lines[i] == '') continue;
newText += util.format(s, lines[i]) + '\n';
}
return newText.trimEnd();
});
}
const watchedFiles: Record<string, Stats> = {};
/**
* Watch a file and trigger callback on change.
*
* We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes
*/
function watchFile(filepath: string, cb: (filepath: string) => void) {
watch(filepath, () => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
}
watchedFiles[filepath] = stat;
});
}
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: FormInput[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}

685
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.0.5", features = [] }
tauri-build = { version = "2.0.6", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
@@ -42,7 +42,6 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
[dependencies]
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0"
@@ -59,19 +58,19 @@ rustls-platform-verifier = "0.5.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-log = { version = "2.2.1", features = ["colored"] }
tauri-plugin-opener = "2.2.5"
tauri-plugin-os = "2.2.0"
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
tauri-plugin-opener = "2.2.6"
tauri-plugin-os = "2.2.1"
tauri-plugin-shell = { workspace = true }
tauri-plugin-single-instance = "2.2.1"
tauri-plugin-updater = "2.4.0"
tauri-plugin-single-instance = "2.2.2"
tauri-plugin-updater = "2.6.1"
tauri-plugin-window-state = "2.2.1"
tokio = { version = "1.43.0", features = ["sync"] }
tokio-stream = "0.1.17"
ts-rs = { workspace = true }
thiserror = { workspace = true }
uuid = "1.12.1"
yaak-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" }

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AnalyticsAction = "cancel" | "click" | "commit" | "create" | "delete" | "delete_many" | "duplicate" | "error" | "export" | "hide" | "import" | "launch" | "launch_first" | "launch_update" | "send" | "show" | "toggle" | "update" | "upsert";
export type AnalyticsResource = "app" | "appearance" | "button" | "checkbox" | "cookie_jar" | "dialog" | "environment" | "folder" | "grpc_connection" | "grpc_event" | "grpc_request" | "http_request" | "http_response" | "key_value" | "link" | "mutation" | "plugin" | "select" | "setting" | "sidebar" | "tab" | "theme" | "websocket_connection" | "websocket_event" | "websocket_request" | "workspace";

File diff suppressed because one or more lines are too long

View File

@@ -5572,6 +5572,11 @@
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-deactivate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
@@ -5582,6 +5587,11 @@
"type": "string",
"const": "yaak-license:deny-check"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -5572,6 +5572,11 @@
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-deactivate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
@@ -5582,6 +5587,11 @@
"type": "string",
"const": "yaak-license:deny-check"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -1,247 +0,0 @@
use std::fmt::Display;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::queries::{
generate_id, get_key_value_int, get_key_value_string, get_or_create_settings,
set_key_value_int, set_key_value_string, UpdateSource,
};
use crate::is_dev;
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
// serializable
#[derive(Serialize, Deserialize, Debug, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "analytics.ts")]
pub enum AnalyticsResource {
App,
Appearance,
Button,
Checkbox,
CookieJar,
Dialog,
Environment,
Folder,
GrpcConnection,
GrpcEvent,
GrpcRequest,
HttpRequest,
HttpResponse,
KeyValue,
Link,
Mutation,
Plugin,
Select,
Setting,
Sidebar,
Tab,
Theme,
WebsocketConnection,
WebsocketEvent,
WebsocketRequest,
Workspace,
}
impl AnalyticsResource {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
impl Display for AnalyticsResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
}
}
#[derive(Serialize, Deserialize, Debug, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "analytics.ts")]
pub enum AnalyticsAction {
Cancel,
Click,
Commit,
Create,
Delete,
DeleteMany,
Duplicate,
Error,
Export,
Hide,
Import,
Launch,
LaunchFirst,
LaunchUpdate,
Send,
Show,
Toggle,
Update,
Upsert,
}
impl AnalyticsAction {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
impl Display for AnalyticsAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
}
}
#[derive(Default, Debug)]
pub struct LaunchEventInfo {
pub current_version: String,
pub previous_version: String,
pub launched_after_update: bool,
pub num_launches: i32,
}
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
if info.previous_version.is_empty() {
track_event(w, AnalyticsResource::App, AnalyticsAction::LaunchFirst, None).await;
} else {
info.launched_after_update = info.current_version != info.previous_version;
if info.launched_after_update {
track_event(
w,
AnalyticsResource::App,
AnalyticsAction::LaunchUpdate,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
)
.await;
}
};
// Track a launch event in all cases
track_event(
w,
AnalyticsResource::App,
AnalyticsAction::Launch,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
)
.await;
// Update key values
set_key_value_string(
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
&UpdateSource::Background,
)
.await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
.await;
info
}
pub async fn track_event<R: Runtime>(
w: &WebviewWindow<R>,
resource: AnalyticsResource,
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
let info = w.app_handle().package_info();
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
let site = match is_dev() {
true => "site_TkHWjoXwZPq3HfhERb",
false => "site_zOK0d7jeBy2TLxFCnZ",
};
let base_url = match is_dev() {
true => "http://localhost:7194",
false => "https://t.yaak.app",
};
let params = vec![
("u", id),
("e", event.clone()),
("a", attributes_json.clone()),
("id", site.to_string()),
("v", info.version.clone().to_string()),
("os", get_os().to_string()),
("tz", tz),
("xy", get_window_size(w)),
];
let req =
reqwest::Client::builder().build().unwrap().get(format!("{base_url}/t/e")).query(&params);
let settings = get_or_create_settings(w).await;
if !settings.telemetry {
info!("Track event (disabled): {}", event);
return;
}
// Disable analytics actual sending in dev
if is_dev() {
debug!("Track event: {} {}", event, attributes_json);
return;
}
if let Err(e) = req.send().await {
info!("Error sending analytics event: {}", e);
}
}
pub fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
let current_monitor = match w.current_monitor() {
Ok(Some(m)) => m,
_ => return "unknown".to_string(),
};
let scale_factor = current_monitor.scale_factor();
let size = current_monitor.size();
let width: f64 = size.width as f64 / scale_factor;
let height: f64 = size.height as f64 / scale_factor;
format!("{}x{}", (width / 100.0).round() * 100.0, (height / 100.0).round() * 100.0)
}
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
let id = get_key_value_string(w, "analytics", "id", "").await;
if id.is_empty() {
let new_id = generate_id();
set_key_value_string(w, "analytics", "id", new_id.as_str(), &UpdateSource::Background)
.await;
new_id
} else {
id
}
}
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
}

43
src-tauri/src/error.rs Normal file
View File

@@ -0,0 +1,43 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Render error: {0}")]
TemplateError(#[from] yaak_templates::error::Error),
#[error("Model error: {0}")]
ModelError(#[from] yaak_models::error::Error),
#[error("Sync error: {0}")]
SyncError(#[from] yaak_sync::error::Error),
#[error("Git error: {0}")]
GitError(#[from] yaak_git::error::Error),
#[error("Websocket error: {0}")]
WebsocketError(#[from] yaak_ws::error::Error),
#[error("License error: {0}")]
LicenseError(#[from] yaak_license::error::Error),
#[error("Plugin error: {0}")]
PluginError(#[from] yaak_plugins::error::Error),
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),
#[error("Generic error: {0}")]
GenericError(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

64
src-tauri/src/history.rs Normal file
View File

@@ -0,0 +1,64 @@
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{
get_key_value_int, get_key_value_string,
set_key_value_int, set_key_value_string, UpdateSource,
};
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
#[derive(Default, Debug)]
pub struct LaunchEventInfo {
pub current_version: String,
pub previous_version: String,
pub launched_after_update: bool,
pub num_launches: i32,
}
pub async fn store_launch_history<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
if info.previous_version.is_empty() {
} else {
info.launched_after_update = info.current_version != info.previous_version;
};
// Update key values
set_key_value_string(
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
&UpdateSource::Background,
)
.await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
.await;
info
}
pub fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
}

View File

@@ -1,3 +1,5 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::render::render_http_request;
use crate::response_err;
use http::header::{ACCEPT, USER_AGENT};
@@ -43,14 +45,10 @@ pub async fn send_http_request<R: Runtime>(
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
) -> Result<HttpResponse> {
let plugin_manager = window.state::<PluginManager>();
let workspace = get_workspace(window, &unrendered_request.workspace_id)
.await
.expect("Failed to get Workspace");
let base_environment = get_base_environment(window, &unrendered_request.workspace_id)
.await
.expect("Failed to get base environment");
let workspace = get_workspace(window, &unrendered_request.workspace_id).await?;
let base_environment = get_base_environment(window, &unrendered_request.workspace_id).await?;
let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
@@ -61,9 +59,17 @@ pub async fn send_http_request<R: Runtime>(
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let request =
render_http_request(&unrendered_request, &base_environment, environment.as_ref(), &cb)
.await;
let request = match render_http_request(
&unrendered_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await
{
Ok(r) => r,
Err(e) => return Ok(response_err(&*response.lock().await, e.to_string(), window).await),
};
let mut url_string = request.url;
@@ -139,10 +145,9 @@ pub async fn send_http_request<R: Runtime>(
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.map(|c| Ok(c))
.collect::<Vec<Result<_, ()>>>();
.collect::<Vec<Result<_>>>();
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)
.expect("Failed to create cookie store");
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?;
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store);
let cookie_store = Arc::new(cookie_store);
client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store));
@@ -158,7 +163,7 @@ pub async fn send_http_request<R: Runtime>(
));
}
let client = client_builder.build().expect("Failed to build client");
let client = client_builder.build()?;
// Render query parameters
let mut query_params = Vec::new();
@@ -193,8 +198,8 @@ pub async fn send_http_request<R: Runtime>(
}
};
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let m = Method::from_str(&request.method.to_uppercase())
.map_err(|e| GenericError(e.to_string()))?;
let mut request_builder = client.request(m, url).query(&query_params);
let mut headers = HeaderMap::new();
@@ -282,7 +287,7 @@ pub async fn send_http_request<R: Runtime>(
} else if body_type == "binary" && request_body.contains_key("filePath") {
let file_path = request_body
.get("filePath")
.ok_or("filePath not set")?
.ok_or(GenericError("filePath not set".to_string()))?
.as_str()
.unwrap_or_default();
@@ -431,7 +436,7 @@ pub async fn send_http_request<R: Runtime>(
}
}
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
let (resp_tx, resp_rx) = oneshot::channel::<std::result::Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();
let start = std::time::Instant::now();

View File

@@ -1,18 +1,19 @@
extern crate core;
#[cfg(target_os = "macos")]
extern crate objc;
use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::encoding::read_response_body;
use crate::error::Error::GenericError;
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_template};
use crate::updates::{UpdateMode, YaakUpdater};
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use crate::uri_scheme::handle_uri_scheme;
use error::Result as YaakResult;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, warn};
use rand::random;
use regex::Regex;
use serde_json::{json, Value};
use std::collections::{BTreeMap, HashMap};
use std::fs::{create_dir_all, File};
use std::path::PathBuf;
@@ -30,8 +31,31 @@ use tokio::sync::Mutex;
use tokio::task::block_in_place;
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use yaak_models::models::{CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue, ModelType, Plugin, Settings, WebsocketRequest, Workspace, WorkspaceMeta};
use yaak_models::queries::{batch_upsert, cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, delete_all_grpc_connections, delete_all_grpc_connections_for_workspace, delete_all_http_responses_for_request, delete_all_http_responses_for_workspace, delete_all_websocket_connections_for_workspace, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request, duplicate_http_request, ensure_base_environment, generate_model_id, get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, upsert_workspace_meta, BatchUpsertResult, UpdateSource};
use yaak_models::models::{
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
ModelType, Plugin, Settings, WebsocketRequest, Workspace, WorkspaceMeta,
};
use yaak_models::queries::{
batch_upsert, cancel_pending_grpc_connections, cancel_pending_http_responses,
cancel_pending_websocket_connections, create_default_http_response,
delete_all_grpc_connections, delete_all_grpc_connections_for_workspace,
delete_all_http_responses_for_request, delete_all_http_responses_for_workspace,
delete_all_websocket_connections_for_workspace, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_plugin, delete_workspace, duplicate_folder,
duplicate_grpc_request, duplicate_http_request, ensure_base_environment, generate_model_id,
get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace,
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces,
set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
};
use yaak_plugins::events::{
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
@@ -44,9 +68,10 @@ use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json;
use yaak_templates::{Parser, Tokens};
mod analytics;
mod encoding;
mod error;
mod grpc;
mod history;
mod http_request;
mod notifications;
mod plugin_events;
@@ -54,6 +79,7 @@ mod render;
#[cfg(target_os = "macos")]
mod tauri_plugin_mac_window;
mod updates;
mod uri_scheme;
mod window;
mod window_menu;
@@ -90,8 +116,8 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
}
#[tauri::command]
async fn cmd_parse_template(template: &str) -> Result<Tokens, String> {
Ok(Parser::new(template).parse())
async fn cmd_parse_template(template: &str) -> YaakResult<Tokens> {
Ok(Parser::new(template).parse()?)
}
#[tauri::command]
@@ -106,14 +132,13 @@ async fn cmd_render_template<R: Runtime>(
template: &str,
workspace_id: &str,
environment_id: Option<&str>,
) -> Result<String, String> {
) -> YaakResult<String> {
let environment = match environment_id {
Some(id) => Some(get_environment(&window, id).await.map_err(|e| e.to_string())?),
Some(id) => get_environment(&window, id).await.ok(),
None => None,
};
let base_environment =
get_base_environment(&window, &workspace_id).await.map_err(|e| e.to_string())?;
let rendered = render_template(
let base_environment = get_base_environment(&window, &workspace_id).await?;
let result = render_template(
template,
&base_environment,
environment.as_ref(),
@@ -123,8 +148,8 @@ async fn cmd_render_template<R: Runtime>(
RenderPurpose::Preview,
),
)
.await;
Ok(rendered)
.await?;
Ok(result)
}
#[tauri::command]
@@ -169,18 +194,15 @@ async fn cmd_grpc_go<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
) -> YaakResult<String> {
let environment = match environment_id {
Some(id) => Some(get_environment(&window, id).await.map_err(|e| e.to_string())?),
Some(id) => get_environment(&window, id).await.ok(),
None => None,
};
let unrendered_request = get_grpc_request(&window, request_id)
.await
.map_err(|e| e.to_string())?
.ok_or("Failed to find GRPC request")?;
let base_environment = get_base_environment(&window, &unrendered_request.workspace_id)
.await
.map_err(|e| e.to_string())?;
.await?
.ok_or(GenericError("Failed to get GRPC request".to_string()))?;
let base_environment = get_base_environment(&window, &unrendered_request.workspace_id).await?;
let request = render_grpc_request(
&unrendered_request,
&base_environment,
@@ -191,7 +213,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await?;
let mut metadata = BTreeMap::new();
// Add the rest of metadata
@@ -222,33 +244,27 @@ async fn cmd_grpc_go<R: Runtime>(
})
.collect(),
};
let plugin_result = plugin_manager
.call_http_authentication(&window, &auth_name, plugin_req)
.await
.map_err(|e| e.to_string())?;
let plugin_result =
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
for header in plugin_result.set_headers {
metadata.insert(header.name, header.value);
}
}
let conn = {
let req = request.clone();
upsert_grpc_connection(
&window,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
status: -1,
elapsed: 0,
state: GrpcConnectionState::Initialized,
url: req.url.clone(),
..Default::default()
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?
};
let conn = upsert_grpc_connection(
&window,
&GrpcConnection {
workspace_id: request.workspace_id.clone(),
request_id: request.id.clone(),
status: -1,
elapsed: 0,
state: GrpcConnectionState::Initialized,
url: request.url.clone(),
..Default::default()
},
&UpdateSource::Window,
)
.await?;
let conn_id = conn.id.clone();
@@ -271,7 +287,7 @@ async fn cmd_grpc_go<R: Runtime>(
let req = request.clone();
match (req.service, req.method) {
(Some(service), Some(method)) => (service, method),
_ => return Err("Service and method are required".to_string()),
_ => return Err(GenericError("Service and method are required".to_string())),
}
};
@@ -299,13 +315,13 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?;
.await?;
return Ok(conn_id);
}
};
let method_desc = connection.method(&service, &method).map_err(|e| e.to_string())?;
let method_desc =
connection.method(&service, &method).map_err(|e| GenericError(e.to_string()))?;
#[derive(serde::Deserialize)]
enum IncomingMsg {
@@ -342,23 +358,22 @@ async fn cmd_grpc_go<R: Runtime>(
let window = window.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let msg = {
block_in_place(|| {
tauri::async_runtime::block_on(async {
render_template(
msg.as_str(),
&workspace,
environment.as_ref(),
&PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(&window),
RenderPurpose::Send,
),
)
.await
})
let msg = block_in_place(|| {
tauri::async_runtime::block_on(async {
render_template(
msg.as_str(),
&workspace,
environment.as_ref(),
&PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(&window),
RenderPurpose::Send,
),
)
.await
.expect("Failed to render template")
})
};
});
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{
Ok(d_msg) => d_msg,
@@ -423,7 +438,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await?;
upsert_grpc_event(
&window,
@@ -435,8 +450,7 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await?;
async move {
let (maybe_stream, maybe_msg) =
@@ -718,7 +732,7 @@ async fn cmd_send_ephemeral_request(
environment_id: Option<&str>,
cookie_jar_id: Option<&str>,
window: WebviewWindow,
) -> Result<HttpResponse, String> {
) -> YaakResult<HttpResponse> {
let response = HttpResponse::new();
request.id = "".to_string();
let environment = match environment_id {
@@ -807,7 +821,7 @@ async fn cmd_import_data<R: Runtime>(
.await
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let (import_result, plugin_name) =
let import_result =
plugin_manager.import_data(&window, file_contents).await.map_err(|e| e.to_string())?;
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
@@ -923,14 +937,6 @@ async fn cmd_import_data<R: Runtime>(
.await
.map_err(|e| e.to_string())?;
analytics::track_event(
&window,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
Ok(upserted)
}
@@ -1007,17 +1013,9 @@ async fn cmd_curl_to_request<R: Runtime>(
plugin_manager: State<'_, PluginManager>,
workspace_id: &str,
) -> Result<HttpRequest, String> {
let (import_result, plugin_name) =
let import_result =
{ plugin_manager.import_data(&window, command).await.map_err(|e| e.to_string())? };
analytics::track_event(
&window,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
import_result.resources.http_requests.get(0).ok_or("No curl command found".to_string()).map(
|r| {
let mut request = r.clone();
@@ -1051,8 +1049,6 @@ async fn cmd_export_data(
f.sync_all().expect("Failed to sync");
analytics::track_event(&window, AnalyticsResource::App, AnalyticsAction::Export, None).await;
Ok(())
}
@@ -1085,10 +1081,9 @@ async fn cmd_send_http_request(
// condition where the user may have just edited a field before sending
// that has not yet been saved in the DB.
request: HttpRequest,
) -> Result<HttpResponse, String> {
let response = create_default_http_response(&window, &request.id, &UpdateSource::Window)
.await
.map_err(|e| e.to_string())?;
) -> YaakResult<HttpResponse> {
let response =
create_default_http_response(&window, &request.id, &UpdateSource::Window).await?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(format!("cancel_http_response_{}", response.id), move |_event| {
@@ -1131,28 +1126,6 @@ async fn response_err<R: Runtime>(
response
}
#[tauri::command]
async fn cmd_track_event(
window: WebviewWindow,
resource: &str,
action: &str,
attributes: Option<Value>,
) -> Result<(), String> {
match (AnalyticsResource::from_str(resource), AnalyticsAction::from_str(action)) {
(Ok(resource), Ok(action)) => {
analytics::track_event(&window, resource, action, attributes).await;
}
(r, a) => {
error!(
"Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}",
r, a
);
return Err("Invalid analytics event".to_string());
}
};
Ok(())
}
#[tauri::command]
async fn cmd_set_update_mode(update_mode: &str, w: WebviewWindow) -> Result<KeyValue, String> {
cmd_set_key_value("app", "update_mode", update_mode, w).await.map_err(|e| e.to_string())
@@ -1678,8 +1651,8 @@ async fn cmd_new_child_window(
url,
inner_size: Some(inner_size),
position: Some(position),
navigation_tx: None,
hide_titlebar: true,
..Default::default()
};
let child_window = window::create_window(&app_handle, config);
@@ -1739,7 +1712,12 @@ async fn cmd_check_for_updates(
yaak_updater: State<'_, Mutex<YaakUpdater>>,
) -> Result<bool, String> {
let update_mode = get_update_mode(&app_handle).await;
yaak_updater.lock().await.force_check(&app_handle, update_mode).await.map_err(|e| e.to_string())
yaak_updater
.lock()
.await
.check_now(&app_handle, update_mode, UpdateTrigger::User)
.await
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -1918,7 +1896,6 @@ pub fn run() {
cmd_set_update_mode,
cmd_template_functions,
cmd_template_tokens_to_string,
cmd_track_event,
cmd_uninstall_plugin,
cmd_update_cookie_jar,
cmd_update_environment,
@@ -1930,10 +1907,7 @@ pub fn run() {
cmd_update_workspace_meta,
cmd_write_file_dev,
])
.register_uri_scheme_protocol("yaak", |_app, _req| {
debug!("Testing yaak protocol");
tauri::http::Response::builder().body("Success".as_bytes().to_vec()).unwrap()
})
.register_uri_scheme_protocol("yaak", handle_uri_scheme)
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| {
@@ -1941,15 +1915,16 @@ pub fn run() {
RunEvent::Ready => {
let w = create_main_window(app_handle, "/");
tauri::async_runtime::spawn(async move {
let info = analytics::track_launch_event(&w).await;
let info = history::store_launch_history(&w).await;
debug!("Launched Yaak {:?}", info);
});
// Cancel pending requests
let h = app_handle.clone();
tauri::async_runtime::block_on(async move {
let _ = cancel_pending_responses(&h).await;
let _ = cancel_pending_http_responses(&h).await;
let _ = cancel_pending_grpc_connections(&h).await;
let _ = cancel_pending_websocket_connections(&h).await;
});
}
RunEvent::WindowEvent {
@@ -1961,7 +1936,7 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
let val: State<'_, Mutex<YaakUpdater>> = h.state();
let update_mode = get_update_mode(&h).await;
if let Err(e) = val.lock().await.check(&h, update_mode).await {
if let Err(e) = val.lock().await.maybe_check(&h, update_mode).await {
warn!("Failed to check for updates {e:?}");
};
});
@@ -2030,8 +2005,8 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0,
)),
navigation_tx: None,
hide_titlebar: true,
..Default::default()
};
window::create_window(handle, config)
@@ -2070,7 +2045,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
crate::plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
});
}
plugin_manager.unsubscribe(rx_id.as_str()).await;

View File

@@ -1,6 +1,6 @@
use std::time::SystemTime;
use crate::analytics::{get_num_launches, get_os};
use crate::history::{get_num_launches, get_os};
use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
@@ -93,11 +93,12 @@ impl YaakNotifier {
let seen = get_kv(window).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
continue;
}
debug!("Got notification {:?}", notification);
let _ = window.emit_to(window.label(), "notification", notification.clone());
break; // Only show one notification
}
Ok(())

View File

@@ -30,7 +30,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
// debug!("Got event to app {event:?}");
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
@@ -90,7 +90,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(),
&cb,
)
.await;
.await
.expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
@@ -107,8 +108,9 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb)
.await
.expect("Failed to render template");
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
@@ -204,32 +206,55 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}
InternalEventPayload::OpenWindowRequest(req) => {
let label = req.label;
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
let (navigation_tx, mut navigation_rx) = tokio::sync::mpsc::channel(128);
let (close_tx, mut close_rx) = tokio::sync::mpsc::channel(128);
let win_config = CreateWindowConfig {
url: &req.url,
label: &label.clone(),
title: &req.title.unwrap_or_default(),
navigation_tx: Some(tx),
navigation_tx: Some(navigation_tx),
close_tx: Some(close_tx),
inner_size: req.size.map(|s| (s.width, s.height)),
position: None,
hide_titlebar: false,
data_dir_key: req.data_dir_key,
..Default::default()
};
create_window(app_handle, win_config);
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = rx.recv().await {
let label = label.clone();
let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let label = label.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = navigation_rx.recv().await {
let url = url.to_string();
let label = label.clone();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
}
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let label = label.clone();
tauri::async_runtime::spawn(async move {
while let Some(_) = close_rx.recv().await {
let label = label.clone();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowCloseEvent,
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
}
None
}
InternalEventPayload::CloseWindowRequest(req) => {

View File

@@ -12,7 +12,7 @@ pub async fn render_template<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
let vars = &make_vars_hashmap(base_environment, environment);
render(template, vars, cb).await
}
@@ -22,7 +22,7 @@ pub async fn render_json_value<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> Value {
) -> yaak_templates::error::Result<Value> {
let vars = &make_vars_hashmap(base_environment, environment);
render_json_value_raw(value, vars, cb).await
}
@@ -32,32 +32,32 @@ pub async fn render_grpc_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> GrpcRequest {
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut metadata = Vec::new();
for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(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;
let url = render(r.url.as_str(), vars, cb).await?;
GrpcRequest {
Ok(GrpcRequest {
url,
metadata,
authentication,
..r.to_owned()
}
})
}
pub async fn render_http_request<T: TemplateCallback>(
@@ -65,15 +65,15 @@ pub async fn render_http_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> HttpRequest {
) -> yaak_templates::error::Result<HttpRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() {
url_parameters.push(HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
@@ -82,41 +82,41 @@ pub async fn render_http_request<T: TemplateCallback>(
for p in r.headers.clone() {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut body = BTreeMap::new();
for (k, v) in r.body.clone() {
body.insert(k, render_json_value_raw(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_raw(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;
let url = render(r.url.clone().as_str(), vars, cb).await?;
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
let (url, url_parameters) = apply_path_placeholders(&url, url_parameters);
HttpRequest {
Ok(HttpRequest {
url,
url_parameters,
headers,
body,
authentication,
..r.to_owned()
}
})
}
pub async fn render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
parse_and_render(template, vars, cb).await
}

View File

@@ -6,6 +6,7 @@ use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;
use tokio::task::block_in_place;
use yaak_models::queries::get_or_create_settings;
use yaak_plugins::manager::PluginManager;
use crate::is_dev;
@@ -46,6 +47,11 @@ impl UpdateMode {
}
}
pub enum UpdateTrigger {
Background,
User,
}
impl YaakUpdater {
pub fn new() -> Self {
Self {
@@ -53,11 +59,14 @@ impl YaakUpdater {
}
}
pub async fn force_check(
pub async fn check_now(
&mut self,
app_handle: &AppHandle,
mode: UpdateMode,
update_trigger: UpdateTrigger,
) -> Result<bool, tauri_plugin_updater::Error> {
let settings = get_or_create_settings(app_handle).await;
let update_key = format!("{:x}", md5::compute(settings.id));
self.last_update_check = SystemTime::now();
info!("Checking for updates mode={}", mode);
@@ -79,6 +88,14 @@ impl YaakUpdater {
});
})
.header("X-Update-Mode", mode.to_string())?
.header("X-Update-Key", update_key)?
.header(
"X-Update-Trigger",
match update_trigger {
UpdateTrigger::Background => "background",
UpdateTrigger::User => "user",
},
)?
.build()?
.check()
.await;
@@ -129,7 +146,7 @@ impl YaakUpdater {
Err(e) => Err(e),
}
}
pub async fn check(
pub async fn maybe_check(
&mut self,
app_handle: &AppHandle,
mode: UpdateMode,
@@ -150,6 +167,6 @@ impl YaakUpdater {
return Ok(false);
}
self.force_check(app_handle, mode).await
self.check_now(app_handle, mode, UpdateTrigger::Background).await
}
}

View File

@@ -0,0 +1,25 @@
use log::{info, warn};
use tauri::{Manager, Runtime, UriSchemeContext};
pub(crate) fn handle_uri_scheme<R: Runtime>(
a: UriSchemeContext<R>,
req: http::Request<Vec<u8>>,
) -> http::Response<Vec<u8>> {
println!("------------- Yaak URI scheme invoked!");
let uri = req.uri();
let window = a
.app_handle()
.get_webview_window(a.webview_label())
.expect("Failed to get webview window for URI scheme event");
info!("Yaak URI scheme invoked with {uri:?} {window:?}");
let path = uri.path();
if path == "/data/import" {
warn!("TODO: import data")
} else if path == "/plugins/install" {
warn!("TODO: install plugin")
}
let msg = format!("No handler found for {path}");
tauri::http::Response::builder().status(404).body(msg.as_bytes().to_vec()).unwrap()
}

View File

@@ -3,7 +3,7 @@ use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_
use log::{info, warn};
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow,
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
@@ -16,6 +16,8 @@ pub(crate) struct CreateWindowConfig<'s> {
pub inner_size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub navigation_tx: Option<mpsc::Sender<String>>,
pub close_tx: Option<mpsc::Sender<()>>,
pub data_dir_key: Option<String>,
pub hide_titlebar: bool,
}
@@ -41,6 +43,22 @@ pub(crate) fn create_window<R: Runtime>(
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some(key) = config.data_dir_key {
#[cfg(not(target_os = "macos"))]
{
use std::fs;
let dir = handle.path().temp_dir().unwrap().join("yaak_sessions").join(key);
fs::create_dir_all(dir.clone()).unwrap();
win_builder = win_builder.data_directory(dir);
}
// macOS doesn't support data dir so must use this fn instead
#[cfg(target_os = "macos")]
{
win_builder = win_builder.data_store_identifier(to_fixed_hash(&key));
}
}
if let Some((w, h)) = config.inner_size {
win_builder = win_builder.inner_size(w, h);
} else {
@@ -85,6 +103,18 @@ pub(crate) fn create_window<R: Runtime>(
let win = win_builder.build().unwrap();
if let Some(tx) = config.close_tx {
win.on_window_event(move |event| match event {
WindowEvent::CloseRequested { .. } => {
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(()).await.unwrap();
});
}
_ => {}
});
}
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
@@ -128,3 +158,10 @@ pub(crate) fn create_window<R: Runtime>(
win
}
fn to_fixed_hash(s: &str) -> [u8; 16] {
let hash = md5::compute(s.as_bytes());
let mut fixed = [0u8; 16];
fixed.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash
fixed
}

View File

@@ -1044,6 +1044,7 @@ var require_re = __commonJS({
var re = exports2.re = [];
var safeRe = exports2.safeRe = [];
var src = exports2.src = [];
var safeSrc = exports2.safeSrc = [];
var t = exports2.t = {};
var R = 0;
var LETTERDASHNUMBER = "[a-zA-Z0-9-]";
@@ -1064,6 +1065,7 @@ var require_re = __commonJS({
debug(name, index, value);
t[name] = index;
src[index] = value;
safeSrc[index] = safe;
re[index] = new RegExp(value, isGlobal ? "g" : void 0);
safeRe[index] = new RegExp(safe, isGlobal ? "g" : void 0);
};
@@ -1160,7 +1162,7 @@ var require_semver = __commonJS({
"../../node_modules/semver/classes/semver.js"(exports2, module2) {
var debug = require_debug();
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants();
var { safeRe: re, t } = require_re();
var { safeRe: re, safeSrc: src, t } = require_re();
var parseOptions = require_parse_options();
var { compareIdentifiers } = require_identifiers();
var SemVer = class _SemVer {
@@ -1300,6 +1302,18 @@ var require_semver = __commonJS({
// preminor will bump the version up to the next minor release, and immediately
// down to pre-release. premajor and prepatch work the same way.
inc(release, identifier, identifierBase) {
if (release.startsWith("pre")) {
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (identifier) {
const r = new RegExp(`^${this.options.loose ? src[t.PRERELEASELOOSE] : src[t.PRERELEASE]}$`);
const match = `-${identifier}`.match(r);
if (!match || match[1] !== identifier) {
throw new Error(`invalid identifier: ${identifier}`);
}
}
}
switch (release) {
case "premajor":
this.prerelease.length = 0;
@@ -1325,6 +1339,12 @@ var require_semver = __commonJS({
}
this.inc("pre", identifier, identifierBase);
break;
case "release":
if (this.prerelease.length === 0) {
throw new Error(`version ${this.raw} is not a prerelease`);
}
this.prerelease.length = 0;
break;
case "major":
if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) {
this.major++;
@@ -1348,9 +1368,6 @@ var require_semver = __commonJS({
break;
case "pre": {
const base = Number(identifierBase) ? 1 : 0;
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (this.prerelease.length === 0) {
this.prerelease = [base];
} else {
@@ -1485,13 +1502,12 @@ var require_diff = __commonJS({
if (!lowVersion.patch && !lowVersion.minor) {
return "major";
}
if (highVersion.patch) {
if (lowVersion.compareMain(highVersion) === 0) {
if (lowVersion.minor && !lowVersion.patch) {
return "minor";
}
return "patch";
}
if (highVersion.minor) {
return "minor";
}
return "major";
}
const prefix = highHasPre ? "pre" : "";
if (v1.major !== v2.major) {

View File

@@ -102,9 +102,20 @@ async function getToken(ctx, contextId) {
async function deleteToken(ctx, contextId) {
return ctx.store.delete(tokenStoreKey(contextId));
}
async function resetDataDirKey(ctx, contextId) {
const key = (/* @__PURE__ */ new Date()).toISOString();
return ctx.store.set(dataDirStoreKey(contextId), key);
}
async function getDataDirKey(ctx, contextId) {
const key = await ctx.store.get(dataDirStoreKey(contextId)) ?? "default";
return `${contextId}::${key}`;
}
function tokenStoreKey(context_id) {
return ["token", context_id].join("::");
}
function dataDirStoreKey(context_id) {
return ["data_dir", context_id].join("::");
}
// src/getOrRefreshAccessToken.ts
async function getOrRefreshAccessToken(ctx, contextId, {
@@ -119,7 +130,7 @@ async function getOrRefreshAccessToken(ctx, contextId, {
if (token == null) {
return null;
}
const now = Date.now() / 1e3;
const now = Date.now();
const isExpired = token.expiresAt && now > token.expiresAt;
if (!isExpired && !forceRefresh) {
return token;
@@ -218,9 +229,16 @@ async function getAuthorizationCode(ctx, contextId, {
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
console.log("Authorizing", authorizationUrlStr);
let foundCode = false;
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
dataDirKey: await getDataDirKey(ctx, contextId),
async onClose() {
if (!foundCode) {
reject(new Error("Authorization window closed"));
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
@@ -230,6 +248,7 @@ async function getAuthorizationCode(ctx, contextId, {
if (!code) {
return;
}
foundCode = true;
close();
const response = await getAccessToken(ctx, {
grantType: "authorization_code",
@@ -428,7 +447,6 @@ var plugin = {
actions: [
{
label: "Copy Current Token",
icon: "copy",
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
@@ -441,7 +459,6 @@ var plugin = {
},
{
label: "Delete Token",
icon: "trash",
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
await ctx.toast.show({ message: "Token deleted", color: "success" });
@@ -449,6 +466,12 @@ var plugin = {
await ctx.toast.show({ message: "No token to delete", color: "warning" });
}
}
},
{
label: "Clear Window Session",
async onSelect(ctx, { contextId }) {
await resetDataDirKey(ctx, contextId);
}
}
],
args: [
@@ -461,17 +484,24 @@ var plugin = {
options: grantTypes
},
// Always-present fields
{ type: "text", name: "clientId", label: "Client ID" },
{
type: "text",
name: "clientId",
label: "Client ID",
optional: true
},
{
type: "text",
name: "clientSecret",
label: "Client Secret",
optional: true,
password: true,
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
},
{
type: "text",
name: "authorizationUrl",
optional: true,
label: "Authorization URL",
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
placeholder: authorizationUrls[0],
@@ -480,6 +510,7 @@ var plugin = {
{
type: "text",
name: "accessTokenUrl",
optional: true,
label: "Access Token URL",
placeholder: accessTokenUrls[0],
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
@@ -590,55 +621,55 @@ var plugin = {
}
],
async onApply(ctx, { values, contextId }) {
const headerPrefix = optionalString(values, "headerPrefix") ?? "";
const grantType = requiredString(values, "grantType");
const headerPrefix = stringArg(values, "headerPrefix");
const grantType = stringArg(values, "grantType");
const credentialsInBody = values.credentials === "body";
let token;
if (grantType === "authorization_code") {
const authorizationUrl = requiredString(values, "authorizationUrl");
const accessTokenUrl = requiredString(values, "accessTokenUrl");
const authorizationUrl = stringArg(values, "authorizationUrl");
const accessTokenUrl = stringArg(values, "accessTokenUrl");
token = await getAuthorizationCode(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
redirectUri: optionalString(values, "redirectUri"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state"),
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
redirectUri: stringArgOrNull(values, "redirectUri"),
scope: stringArgOrNull(values, "scope"),
state: stringArgOrNull(values, "state"),
credentialsInBody,
pkce: values.usePkce ? {
challengeMethod: requiredString(values, "pkceChallengeMethod"),
codeVerifier: optionalString(values, "pkceCodeVerifier")
challengeMethod: stringArg(values, "pkceChallengeMethod"),
codeVerifier: stringArgOrNull(values, "pkceCodeVerifier")
} : null
});
} else if (grantType === "implicit") {
const authorizationUrl = requiredString(values, "authorizationUrl");
const authorizationUrl = stringArg(values, "authorizationUrl");
token = await getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
redirectUri: optionalString(values, "redirectUri"),
responseType: requiredString(values, "responseType"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state")
clientId: stringArg(values, "clientId"),
redirectUri: stringArgOrNull(values, "redirectUri"),
responseType: stringArg(values, "responseType"),
scope: stringArgOrNull(values, "scope"),
state: stringArgOrNull(values, "state")
});
} else if (grantType === "client_credentials") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
const accessTokenUrl = stringArg(values, "accessTokenUrl");
token = await getClientCredentials(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
scope: optionalString(values, "scope"),
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
scope: stringArgOrNull(values, "scope"),
credentialsInBody
});
} else if (grantType === "password") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
const accessTokenUrl = stringArg(values, "accessTokenUrl");
token = await getPassword(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
username: requiredString(values, "username"),
password: requiredString(values, "password"),
scope: optionalString(values, "scope"),
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
username: stringArg(values, "username"),
password: stringArg(values, "password"),
scope: stringArgOrNull(values, "scope"),
credentialsInBody
});
} else {
@@ -654,14 +685,14 @@ var plugin = {
}
}
};
function optionalString(values, name) {
function stringArgOrNull(values, name) {
const arg = values[name];
if (arg == null || arg == "") return null;
return `${arg}`;
}
function requiredString(values, name) {
const arg = optionalString(values, name);
if (!arg) throw new Error(`Missing required argument ${name}`);
function stringArg(values, name) {
const arg = stringArgOrNull(values, name);
if (!arg) return "";
return arg;
}
// Annotate the CommonJS export names for ESM import in node:

View File

@@ -36,6 +36,9 @@ var require_quote = __commonJS({
"use strict";
module2.exports = function quote(xs) {
return xs.map(function(s) {
if (s === "") {
return "''";
}
if (s && typeof s === "object") {
return s.op.replace(/(.)/g, "\\$1");
}

View File

@@ -1304,21 +1304,29 @@ ${indent}`) + "'";
start = start.replace(/\n+/g, `$&${indent}`);
}
const indentSize = indent ? "2" : "1";
let header = (literal ? "|" : ">") + (startWithSpace ? indentSize : "") + chomp;
let header = (startWithSpace ? indentSize : "") + chomp;
if (comment) {
header += " " + commentString(comment.replace(/ ?[\r\n]+/g, " "));
if (onComment)
onComment();
}
if (literal) {
value = value.replace(/\n+/g, `$&${indent}`);
return `${header}
${indent}${start}${value}${end}`;
}
value = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`);
const body = foldFlowLines.foldFlowLines(`${start}${value}${end}`, indent, foldFlowLines.FOLD_BLOCK, getFoldOptions(ctx, true));
return `${header}
if (!literal) {
const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`);
let literalFallback = false;
const foldOptions = getFoldOptions(ctx, true);
if (blockQuote !== "folded" && type !== Scalar.Scalar.BLOCK_FOLDED) {
foldOptions.onOverflow = () => {
literalFallback = true;
};
}
const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions);
if (!literalFallback)
return `>${header}
${indent}${body}`;
}
value = value.replace(/\n+/g, `$&${indent}`);
return `|${header}
${indent}${start}${value}${end}`;
}
function plainString(item, ctx, onComment, onChompKeep) {
const { type, value } = item;
@@ -1446,7 +1454,12 @@ var require_stringify = __commonJS({
let obj;
if (identity.isScalar(item)) {
obj = item.value;
const match = tags.filter((t) => t.identify?.(obj));
let match = tags.filter((t) => t.identify?.(obj));
if (match.length > 1) {
const testMatch = match.filter((t) => t.test);
if (testMatch.length > 0)
match = testMatch;
}
tagObj = match.find((t) => t.format === item.format) ?? match.find((t) => !t.format);
} else {
obj = item;
@@ -1643,14 +1656,15 @@ ${ctx.indent}`;
var require_log = __commonJS({
"../../node_modules/yaml/dist/log.js"(exports2) {
"use strict";
var node_process = require("node:process");
function debug(logLevel, ...messages) {
if (logLevel === "debug")
console.log(...messages);
}
function warn(logLevel, warning) {
if (logLevel === "debug" || logLevel === "warn") {
if (typeof process !== "undefined" && process.emitWarning)
process.emitWarning(warning);
if (typeof node_process.emitWarning === "function")
node_process.emitWarning(warning);
else
console.warn(warning);
}
@@ -1660,51 +1674,36 @@ var require_log = __commonJS({
}
});
// ../../node_modules/yaml/dist/nodes/addPairToJSMap.js
var require_addPairToJSMap = __commonJS({
"../../node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
// ../../node_modules/yaml/dist/schema/yaml-1.1/merge.js
var require_merge = __commonJS({
"../../node_modules/yaml/dist/schema/yaml-1.1/merge.js"(exports2) {
"use strict";
var log = require_log();
var stringify = require_stringify();
var identity = require_identity();
var Scalar = require_Scalar();
var toJS = require_toJS();
var MERGE_KEY = "<<";
function addPairToJSMap(ctx, map, { key, value }) {
if (ctx?.doc.schema.merge && isMergeKey(key)) {
value = identity.isAlias(value) ? value.resolve(ctx.doc) : value;
if (identity.isSeq(value))
for (const it of value.items)
mergeToJSMap(ctx, map, it);
else if (Array.isArray(value))
for (const it of value)
mergeToJSMap(ctx, map, it);
else
mergeToJSMap(ctx, map, value);
} else {
const jsKey = toJS.toJS(key, "", ctx);
if (map instanceof Map) {
map.set(jsKey, toJS.toJS(value, jsKey, ctx));
} else if (map instanceof Set) {
map.add(jsKey);
} else {
const stringKey = stringifyKey(key, jsKey, ctx);
const jsValue = toJS.toJS(value, stringKey, ctx);
if (stringKey in map)
Object.defineProperty(map, stringKey, {
value: jsValue,
writable: true,
enumerable: true,
configurable: true
});
else
map[stringKey] = jsValue;
}
}
return map;
var merge = {
identify: (value) => value === MERGE_KEY || typeof value === "symbol" && value.description === MERGE_KEY,
default: "key",
tag: "tag:yaml.org,2002:merge",
test: /^<<$/,
resolve: () => Object.assign(new Scalar.Scalar(Symbol(MERGE_KEY)), {
addToJSMap: addMergeToJSMap
}),
stringify: () => MERGE_KEY
};
var isMergeKey = (ctx, key) => (merge.identify(key) || identity.isScalar(key) && (!key.type || key.type === Scalar.Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default);
function addMergeToJSMap(ctx, map, value) {
value = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value;
if (identity.isSeq(value))
for (const it of value.items)
mergeValue(ctx, map, it);
else if (Array.isArray(value))
for (const it of value)
mergeValue(ctx, map, it);
else
mergeValue(ctx, map, value);
}
var isMergeKey = (key) => key === MERGE_KEY || identity.isScalar(key) && key.value === MERGE_KEY && (!key.type || key.type === Scalar.Scalar.PLAIN);
function mergeToJSMap(ctx, map, value) {
function mergeValue(ctx, map, value) {
const source = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value;
if (!identity.isMap(source))
throw new Error("Merge sources must be maps or map aliases");
@@ -1726,6 +1725,48 @@ var require_addPairToJSMap = __commonJS({
}
return map;
}
exports2.addMergeToJSMap = addMergeToJSMap;
exports2.isMergeKey = isMergeKey;
exports2.merge = merge;
}
});
// ../../node_modules/yaml/dist/nodes/addPairToJSMap.js
var require_addPairToJSMap = __commonJS({
"../../node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
"use strict";
var log = require_log();
var merge = require_merge();
var stringify = require_stringify();
var identity = require_identity();
var toJS = require_toJS();
function addPairToJSMap(ctx, map, { key, value }) {
if (identity.isNode(key) && key.addToJSMap)
key.addToJSMap(ctx, map, value);
else if (merge.isMergeKey(ctx, key))
merge.addMergeToJSMap(ctx, map, value);
else {
const jsKey = toJS.toJS(key, "", ctx);
if (map instanceof Map) {
map.set(jsKey, toJS.toJS(value, jsKey, ctx));
} else if (map instanceof Set) {
map.add(jsKey);
} else {
const stringKey = stringifyKey(key, jsKey, ctx);
const jsValue = toJS.toJS(value, stringKey, ctx);
if (stringKey in map)
Object.defineProperty(map, stringKey, {
value: jsValue,
writable: true,
enumerable: true,
configurable: true
});
else
map[stringKey] = jsValue;
}
}
return map;
}
function stringifyKey(key, jsKey, ctx) {
if (jsKey === null)
return "";
@@ -2481,7 +2522,7 @@ var require_schema2 = __commonJS({
identify: (value) => typeof value === "boolean",
default: true,
tag: "tag:yaml.org,2002:bool",
test: /^true|false$/,
test: /^true$|^false$/,
resolve: (str) => str === "true",
stringify: stringifyJSON
},
@@ -2520,6 +2561,7 @@ var require_schema2 = __commonJS({
var require_binary = __commonJS({
"../../node_modules/yaml/dist/schema/yaml-1.1/binary.js"(exports2) {
"use strict";
var node_buffer = require("node:buffer");
var Scalar = require_Scalar();
var stringifyString = require_stringifyString();
var binary = {
@@ -2536,8 +2578,8 @@ var require_binary = __commonJS({
* document.querySelector('#photo').src = URL.createObjectURL(blob)
*/
resolve(src, onError) {
if (typeof Buffer === "function") {
return Buffer.from(src, "base64");
if (typeof node_buffer.Buffer === "function") {
return node_buffer.Buffer.from(src, "base64");
} else if (typeof atob === "function") {
const str = atob(src.replace(/[\n\r]/g, ""));
const buffer = new Uint8Array(str.length);
@@ -2552,8 +2594,8 @@ var require_binary = __commonJS({
stringify({ comment, type, value }, ctx, onComment, onChompKeep) {
const buf = value;
let str;
if (typeof Buffer === "function") {
str = buf instanceof Buffer ? buf.toString("base64") : Buffer.from(buf.buffer).toString("base64");
if (typeof node_buffer.Buffer === "function") {
str = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64");
} else if (typeof btoa === "function") {
let s = "";
for (let i = 0; i < buf.length; ++i)
@@ -3065,7 +3107,7 @@ var require_timestamp = __commonJS({
}
return new Date(date);
},
stringify: ({ value }) => value.toISOString().replace(/((T00:00)?:00)?\.000Z$/, "")
stringify: ({ value }) => value.toISOString().replace(/(T00:00:00)?\.000Z$/, "")
};
exports2.floatTime = floatTime;
exports2.intTime = intTime;
@@ -3085,6 +3127,7 @@ var require_schema3 = __commonJS({
var bool = require_bool2();
var float = require_float2();
var int = require_int2();
var merge = require_merge();
var omap = require_omap();
var pairs = require_pairs();
var set = require_set();
@@ -3104,6 +3147,7 @@ var require_schema3 = __commonJS({
float.floatExp,
float.float,
binary.binary,
merge.merge,
omap.omap,
pairs.pairs,
set.set,
@@ -3129,6 +3173,7 @@ var require_tags = __commonJS({
var schema = require_schema();
var schema$1 = require_schema2();
var binary = require_binary();
var merge = require_merge();
var omap = require_omap();
var pairs = require_pairs();
var schema$2 = require_schema3();
@@ -3153,6 +3198,7 @@ var require_tags = __commonJS({
intOct: int.intOct,
intTime: timestamp.intTime,
map: map.map,
merge: merge.merge,
null: _null.nullTag,
omap: omap.omap,
pairs: pairs.pairs,
@@ -3162,13 +3208,18 @@ var require_tags = __commonJS({
};
var coreKnownTags = {
"tag:yaml.org,2002:binary": binary.binary,
"tag:yaml.org,2002:merge": merge.merge,
"tag:yaml.org,2002:omap": omap.omap,
"tag:yaml.org,2002:pairs": pairs.pairs,
"tag:yaml.org,2002:set": set.set,
"tag:yaml.org,2002:timestamp": timestamp.timestamp
};
function getTags(customTags, schemaName) {
let tags = schemas.get(schemaName);
function getTags(customTags, schemaName, addMergeTag) {
const schemaTags = schemas.get(schemaName);
if (schemaTags && !customTags) {
return addMergeTag && !schemaTags.includes(merge.merge) ? schemaTags.concat(merge.merge) : schemaTags.slice();
}
let tags = schemaTags;
if (!tags) {
if (Array.isArray(customTags))
tags = [];
@@ -3183,15 +3234,19 @@ var require_tags = __commonJS({
} else if (typeof customTags === "function") {
tags = customTags(tags.slice());
}
return tags.map((tag) => {
if (typeof tag !== "string")
return tag;
const tagObj = tagsByName[tag];
if (tagObj)
return tagObj;
const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", ");
throw new Error(`Unknown custom tag "${tag}"; use one of ${keys}`);
});
if (addMergeTag)
tags = tags.concat(merge.merge);
return tags.reduce((tags2, tag) => {
const tagObj = typeof tag === "string" ? tagsByName[tag] : tag;
if (!tagObj) {
const tagName = JSON.stringify(tag);
const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", ");
throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`);
}
if (!tags2.includes(tagObj))
tags2.push(tagObj);
return tags2;
}, []);
}
exports2.coreKnownTags = coreKnownTags;
exports2.getTags = getTags;
@@ -3211,10 +3266,9 @@ var require_Schema = __commonJS({
var Schema = class _Schema {
constructor({ compat, customTags, merge, resolveKnownTags, schema, sortMapEntries, toStringDefaults }) {
this.compat = Array.isArray(compat) ? tags.getTags(compat, "compat") : compat ? tags.getTags(null, compat) : null;
this.merge = !!merge;
this.name = typeof schema === "string" && schema || "core";
this.knownTags = resolveKnownTags ? tags.coreKnownTags : {};
this.tags = tags.getTags(customTags, this.name);
this.tags = tags.getTags(customTags, this.name, merge);
this.toStringOptions = toStringDefaults ?? null;
Object.defineProperty(this, identity.MAP, { value: map.map });
Object.defineProperty(this, identity.SCALAR, { value: string.string });
@@ -3346,6 +3400,7 @@ var require_Document = __commonJS({
logLevel: "warn",
prettyErrors: true,
strict: true,
stringKeys: false,
uniqueKeys: true,
version: "1.2"
}, options);
@@ -3547,7 +3602,7 @@ var require_Document = __commonJS({
this.directives.yaml.version = "1.1";
else
this.directives = new directives.Directives({ version: "1.1" });
opt = { merge: true, resolveKnownTags: false, schema: "yaml-1.1" };
opt = { resolveKnownTags: false, schema: "yaml-1.1" };
break;
case "1.2":
case "next":
@@ -3555,7 +3610,7 @@ var require_Document = __commonJS({
this.directives.yaml.version = version;
else
this.directives = new directives.Directives({ version });
opt = { merge: false, resolveKnownTags: true, schema: "core" };
opt = { resolveKnownTags: true, schema: "core" };
break;
case null:
if (this.directives)
@@ -3738,7 +3793,7 @@ var require_resolve_props = __commonJS({
if (atNewline) {
if (comment)
comment += token.source;
else
else if (!found || indicator !== "seq-item-ind")
spaceBefore = true;
} else
commentSep += token.source;
@@ -3888,7 +3943,7 @@ var require_util_map_includes = __commonJS({
const { uniqueKeys } = ctx.options;
if (uniqueKeys === false)
return false;
const isEqual = typeof uniqueKeys === "function" ? uniqueKeys : (a, b) => a === b || identity.isScalar(a) && identity.isScalar(b) && a.value === b.value && !(a.value === "<<" && ctx.schema.merge);
const isEqual = typeof uniqueKeys === "function" ? uniqueKeys : (a, b) => a === b || identity.isScalar(a) && identity.isScalar(b) && a.value === b.value;
return items.some((pair) => isEqual(pair.key, search));
}
exports2.mapIncludes = mapIncludes;
@@ -3947,10 +4002,12 @@ var require_resolve_block_map = __commonJS({
} else if (keyProps.found?.indent !== bm.indent) {
onError(offset, "BAD_INDENT", startColMsg);
}
ctx.atKey = true;
const keyStart = keyProps.end;
const keyNode = key ? composeNode(ctx, key, keyProps, onError) : composeEmptyNode(ctx, keyStart, start, null, keyProps, onError);
if (ctx.schema.compat)
utilFlowIndentCheck.flowIndentCheck(bm.indent, key, onError);
ctx.atKey = false;
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
const valueProps = resolveProps.resolveProps(sep ?? [], {
@@ -4013,6 +4070,8 @@ var require_resolve_block_seq = __commonJS({
const seq = new NodeClass(ctx.schema);
if (ctx.atRoot)
ctx.atRoot = false;
if (ctx.atKey)
ctx.atKey = false;
let offset = bs.offset;
let commentEnd = null;
for (const { start, value } of bs.items) {
@@ -4116,6 +4175,8 @@ var require_resolve_flow_collection = __commonJS({
const atRoot = ctx.atRoot;
if (atRoot)
ctx.atRoot = false;
if (ctx.atKey)
ctx.atKey = false;
let offset = fc.offset + fc.start.source.length;
for (let i = 0; i < fc.items.length; ++i) {
const collItem = fc.items[i];
@@ -4191,10 +4252,12 @@ var require_resolve_flow_collection = __commonJS({
if (isBlock(value))
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
} else {
ctx.atKey = true;
const keyStart = props.end;
const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError);
if (isBlock(key))
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
ctx.atKey = false;
const valueProps = resolveProps.resolveProps(sep ?? [], {
flow: fcName,
indicator: "map-value-ind",
@@ -4757,7 +4820,15 @@ var require_compose_scalar = __commonJS({
function composeScalar(ctx, token, tagToken, onError) {
const { value, type, comment, range } = token.type === "block-scalar" ? resolveBlockScalar.resolveBlockScalar(ctx, token, onError) : resolveFlowScalar.resolveFlowScalar(token, ctx.options.strict, onError);
const tagName = tagToken ? ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)) : null;
const tag = tagToken && tagName ? findScalarTagByName(ctx.schema, value, tagName, tagToken, onError) : token.type === "scalar" ? findScalarTagByTest(ctx, value, token, onError) : ctx.schema[identity.SCALAR];
let tag;
if (ctx.options.stringKeys && ctx.atKey) {
tag = ctx.schema[identity.SCALAR];
} else if (tagName)
tag = findScalarTagByName(ctx.schema, value, tagName, tagToken, onError);
else if (token.type === "scalar")
tag = findScalarTagByTest(ctx, value, token, onError);
else
tag = ctx.schema[identity.SCALAR];
let scalar;
try {
const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options);
@@ -4802,8 +4873,8 @@ var require_compose_scalar = __commonJS({
onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, tagName !== "tag:yaml.org,2002:str");
return schema[identity.SCALAR];
}
function findScalarTagByTest({ directives, schema }, value, token, onError) {
const tag = schema.tags.find((tag2) => tag2.default && tag2.test?.test(value)) || schema[identity.SCALAR];
function findScalarTagByTest({ atKey, directives, schema }, value, token, onError) {
const tag = schema.tags.find((tag2) => (tag2.default === true || atKey && tag2.default === "key") && tag2.test?.test(value)) || schema[identity.SCALAR];
if (schema.compat) {
const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
if (tag.tag !== compat.tag) {
@@ -4855,12 +4926,14 @@ var require_compose_node = __commonJS({
"../../node_modules/yaml/dist/compose/compose-node.js"(exports2) {
"use strict";
var Alias = require_Alias();
var identity = require_identity();
var composeCollection = require_compose_collection();
var composeScalar = require_compose_scalar();
var resolveEnd = require_resolve_end();
var utilEmptyScalarPosition = require_util_empty_scalar_position();
var CN = { composeNode, composeEmptyNode };
function composeNode(ctx, token, props, onError) {
const atKey = ctx.atKey;
const { spaceBefore, comment, anchor, tag } = props;
let node;
let isSrcToken = true;
@@ -4894,6 +4967,10 @@ var require_compose_node = __commonJS({
}
if (anchor && node.anchor === "")
onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string");
if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) {
const msg = "With stringKeys, all keys must be strings";
onError(tag ?? token, "NON_STRING_KEY", msg);
}
if (spaceBefore)
node.spaceBefore = true;
if (comment) {
@@ -4957,6 +5034,7 @@ var require_compose_doc = __commonJS({
const opts = Object.assign({ _directives: directives }, options);
const doc = new Document.Document(void 0, opts);
const ctx = {
atKey: false,
atRoot: true,
directives: doc.directives,
options: doc.options,
@@ -4991,6 +5069,7 @@ var require_compose_doc = __commonJS({
var require_composer = __commonJS({
"../../node_modules/yaml/dist/compose/composer.js"(exports2) {
"use strict";
var node_process = require("node:process");
var directives = require_directives();
var Document = require_Document();
var errors = require_errors();
@@ -5106,7 +5185,7 @@ ${cb}` : comment;
}
/** Advance the composer by one CST token. */
*next(token) {
if (process.env.LOG_STREAM)
if (node_process.env.LOG_STREAM)
console.dir(token, { depth: null });
switch (token.type) {
case "directive":
@@ -6211,6 +6290,7 @@ var require_line_counter = __commonJS({
var require_parser = __commonJS({
"../../node_modules/yaml/dist/parse/parser.js"(exports2) {
"use strict";
var node_process = require("node:process");
var cst = require_cst();
var lexer = require_lexer();
function includesToken(list, type) {
@@ -6333,7 +6413,7 @@ var require_parser = __commonJS({
*/
*next(source) {
this.source = source;
if (process.env.LOG_TOKENS)
if (node_process.env.LOG_TOKENS)
console.log("|", cst.prettyToken(source));
if (this.atScalar) {
this.atScalar = false;
@@ -7067,6 +7147,7 @@ var require_public_api = __commonJS({
var Document = require_Document();
var errors = require_errors();
var log = require_log();
var identity = require_identity();
var lineCounter = require_line_counter();
var parser = require_parser();
function parseOptions(options) {
@@ -7144,6 +7225,8 @@ var require_public_api = __commonJS({
if (!keepUndefined)
return void 0;
}
if (identity.isDocument(value) && !_replacer)
return value.toString(options);
return new Document.Document(value, _replacer, options).toString(options);
}
exports2.parse = parse;

View File

@@ -49782,7 +49782,11 @@ var require_property_base = __commonJS({
* @returns {*|undefined}
*/
parent() {
return this && this.__parent && (this.__parent.__parent || this.__parent) || void 0;
let parent = this.__parent;
if (parent && parent._postman_propertyIsList) {
parent = parent.__parent || parent;
}
return parent || void 0;
},
/**
* Accepts an object and sets it as the parent of the current property.
@@ -77141,7 +77145,7 @@ var require_dynamic_variables = __commonJS({
description: "A random avatar image",
generator: () => {
return faker.random.arrayElement([
// eslint-disable-next-line max-len
// eslint-disable-next-line @stylistic/js/max-len
`https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/${faker.datatype.number(1249)}.jpg`,
`https://avatars.githubusercontent.com/u/${faker.datatype.number(1e8)}`
]);
@@ -77494,7 +77498,7 @@ var require_superstring = __commonJS({
* @readOnly
* @type {RegExp}
*/
REGEX_EXTRACT_VARS: /\{\{([^{}]*?)}}/g,
REGEX_EXTRACT_VARS: /{{([^{}]*?)}}/g,
/**
* Defines the number of times the variable substitution mechanism will repeat until all tokens are resolved
*
@@ -77589,7 +77593,7 @@ var require_property = __commonJS({
} else if (typeof value === "object") {
seen.add(value);
for (const key in value) {
if (Object.hasOwnProperty.call(value, key)) {
if (Object.hasOwn(value, key)) {
_findSubstitutions(value[key], seen, result);
}
}
@@ -77894,7 +77898,7 @@ var require_property_list = __commonJS({
before > -1 ? this.members.splice(before, 0, item) : this.members.push(item);
if ((index = item[this._postman_listIndexKey]) && (index = String(index))) {
this._postman_listIndexCaseInsensitive && (index = index.toLowerCase());
if (this._postman_listAllowsMultipleValues && Object.hasOwnProperty.call(this.reference, index)) {
if (this._postman_listAllowsMultipleValues && Object.hasOwn(this.reference, index)) {
!_2.isArray(this.reference[index]) && (this.reference[index] = [this.reference[index]]);
this.reference[index].push(item);
} else {
@@ -78458,7 +78462,7 @@ var require_parser = __commonJS({
var ReplacementTracker = require_replacement_tracker();
var REGEX_ALL_BACKSLASHES = /\\/g;
var REGEX_LEADING_SLASHES = /^\/+/;
var REGEX_ALL_VARIABLES = /{{[^{}]*[.:/?#@&\]][^{}]*}}/g;
var REGEX_ALL_VARIABLES = /{{[^{}]*}}/g;
var HASH_SEPARATOR = "#";
var PATH_SEPARATOR = "/";
var PORT_SEPARATOR = ":";
@@ -78613,6 +78617,7 @@ var require_query_param = __commonJS({
"../../node_modules/postman-collection/lib/collection/query-param.js"(exports2, module2) {
var _2 = require_util2().lodash;
var Property = require_property().Property;
var Substitutor = require_superstring().Substitutor;
var PropertyList = require_property_list().PropertyList;
var E = "";
var AMPERSAND = "&";
@@ -78623,7 +78628,7 @@ var require_query_param = __commonJS({
var REGEX_HASH = /#/g;
var REGEX_EQUALS = /=/g;
var REGEX_AMPERSAND = /&/g;
var REGEX_EXTRACT_VARS = /{{[^{}]*[&#=][^{}]*}}/g;
var REGEX_EXTRACT_VARS = Substitutor.REGEX_EXTRACT_VARS;
var QueryParam;
var encodeReservedChars = function(str, encodeEquals) {
if (!str) {
@@ -92126,7 +92131,7 @@ var require_db3 = __commonJS({
type: "embed",
format: "pdf"
},
"application/ecmascript": {
"text/javascript": {
type: "text",
format: "script"
},
@@ -92134,6 +92139,66 @@ var require_db3 = __commonJS({
type: "text",
format: "script"
},
"application/ecmascript": {
type: "text",
format: "script"
},
"application/x-ecmascript": {
type: "text",
format: "script"
},
"application/x-javascript": {
type: "text",
format: "script"
},
"text/ecmascript": {
type: "text",
format: "script"
},
"text/javascript1.0": {
type: "text",
format: "script"
},
"text/javascript1.1": {
type: "text",
format: "script"
},
"text/javascript1.2": {
type: "text",
format: "script"
},
"text/javascript1.3": {
type: "text",
format: "script"
},
"text/javascript1.4": {
type: "text",
format: "script"
},
"text/javascript1.5": {
type: "text",
format: "script"
},
"text/jscript": {
type: "text",
format: "script"
},
"text/livescript": {
type: "text",
format: "script"
},
"text/x-ecmascript": {
type: "text",
format: "script"
},
"text/x-javascript": {
type: "text",
format: "script"
},
"text/css": {
type: "text",
format: "stylesheet"
},
"application/json": {
type: "text",
format: "json"
@@ -94086,7 +94151,7 @@ var require_content_info = __commonJS({
* egHeader: inline; filename="test Response.json"
* Reference: https://github.com/jshttp/content-disposition
*/
// eslint-disable-next-line max-len
// eslint-disable-next-line @stylistic/js/max-len
fileNameRegex: /;[ \t]*(?:filename)[ \t]*=[ \t]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[ \t]*/,
/**
* RegExp for extracting filename* from content-disposition header
@@ -94119,7 +94184,7 @@ var require_content_info = __commonJS({
* egHeader: attachment;filename*=utf-8''%E4%BD%A0%E5%A5%BD.txt
* Reference: https://github.com/jshttp/content-disposition
*/
// eslint-disable-next-line max-len, security/detect-unsafe-regex
// eslint-disable-next-line @stylistic/js/max-len, security/detect-unsafe-regex
encodedFileNameRegex: /;[ \t]*(?:filename\*)[ \t]*=[ \t]*([A-Za-z0-9!#$%&+\-^_`{}~]+)'.*'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)[ \t]*/,
/**
* RegExp to match quoted-pair in RFC 2616
@@ -95313,6 +95378,7 @@ var require_re = __commonJS({
var re = exports2.re = [];
var safeRe = exports2.safeRe = [];
var src = exports2.src = [];
var safeSrc = exports2.safeSrc = [];
var t = exports2.t = {};
var R = 0;
var LETTERDASHNUMBER = "[a-zA-Z0-9-]";
@@ -95333,6 +95399,7 @@ var require_re = __commonJS({
debug(name, index, value);
t[name] = index;
src[index] = value;
safeSrc[index] = safe;
re[index] = new RegExp(value, isGlobal ? "g" : void 0);
safeRe[index] = new RegExp(safe, isGlobal ? "g" : void 0);
};
@@ -95429,7 +95496,7 @@ var require_semver = __commonJS({
"../../node_modules/semver/classes/semver.js"(exports2, module2) {
var debug = require_debug();
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants();
var { safeRe: re, t } = require_re();
var { safeRe: re, safeSrc: src, t } = require_re();
var parseOptions = require_parse_options();
var { compareIdentifiers } = require_identifiers();
var SemVer = class _SemVer {
@@ -95569,6 +95636,18 @@ var require_semver = __commonJS({
// preminor will bump the version up to the next minor release, and immediately
// down to pre-release. premajor and prepatch work the same way.
inc(release, identifier, identifierBase) {
if (release.startsWith("pre")) {
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (identifier) {
const r = new RegExp(`^${this.options.loose ? src[t.PRERELEASELOOSE] : src[t.PRERELEASE]}$`);
const match = `-${identifier}`.match(r);
if (!match || match[1] !== identifier) {
throw new Error(`invalid identifier: ${identifier}`);
}
}
}
switch (release) {
case "premajor":
this.prerelease.length = 0;
@@ -95594,6 +95673,12 @@ var require_semver = __commonJS({
}
this.inc("pre", identifier, identifierBase);
break;
case "release":
if (this.prerelease.length === 0) {
throw new Error(`version ${this.raw} is not a prerelease`);
}
this.prerelease.length = 0;
break;
case "major":
if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) {
this.major++;
@@ -95617,9 +95702,6 @@ var require_semver = __commonJS({
break;
case "pre": {
const base = Number(identifierBase) ? 1 : 0;
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (this.prerelease.length === 0) {
this.prerelease = [base];
} else {
@@ -95754,13 +95836,12 @@ var require_diff = __commonJS({
if (!lowVersion.patch && !lowVersion.minor) {
return "major";
}
if (highVersion.patch) {
if (lowVersion.compareMain(highVersion) === 0) {
if (lowVersion.minor && !lowVersion.patch) {
return "minor";
}
return "patch";
}
if (highVersion.minor) {
return "minor";
}
return "major";
}
const prefix = highHasPre ? "pre" : "";
if (v12.major !== v2.major) {
@@ -136895,8 +136976,6 @@ var require_utils3 = __commonJS({
originalRequest.url.query = [];
originalRequest.header = _2.get(response, "originalRequest.headers", []);
originalRequest.body = requestItem.request.body;
response.code = response.code.replace(/X|x/g, "0");
response.code = response.code === "default" ? 500 : _2.toSafeInteger(response.code);
let sdkResponse = new Response({
name: response.name,
code: response.code,
@@ -137692,7 +137771,7 @@ var require_schemaUtils2 = __commonJS({
}
exampleKey = Object.keys(exampleObj)[0];
example = exampleObj[exampleKey];
if (example.$ref) {
if (example && example.$ref) {
example = resolveExampleData(context, example);
}
if (_2.get(example, "value")) {
@@ -137881,6 +137960,65 @@ var require_schemaUtils2 = __commonJS({
}
return schema2;
};
var processSchema = (resolvedSchema) => {
if (resolvedSchema.type === "object" && resolvedSchema.properties) {
const schemaDetails = {
type: resolvedSchema.type,
properties: {},
required: []
}, requiredProperties = new Set(resolvedSchema.required || []);
for (let [propName, propValue] of Object.entries(resolvedSchema.properties)) {
if (!propValue.type) {
continue;
}
const propertyDetails = {
type: propValue.type,
deprecated: propValue.deprecated,
enum: propValue.enum || void 0,
minLength: propValue.minLength,
maxLength: propValue.maxLength,
minimum: propValue.minimum,
maximum: propValue.maximum,
pattern: propValue.pattern,
example: propValue.example,
description: propValue.description,
format: propValue.format
};
if (requiredProperties.has(propName)) {
schemaDetails.required.push(propName);
}
if (propValue.properties) {
let processedProperties = processSchema(propValue);
propertyDetails.properties = processedProperties.properties;
if (processedProperties.required) {
propertyDetails.required = processedProperties.required;
}
} else if (propValue.type === "array" && propValue.items) {
propertyDetails.items = processSchema(propValue.items);
}
schemaDetails.properties[propName] = propertyDetails;
}
if (schemaDetails.required && schemaDetails.required.length === 0) {
schemaDetails.required = void 0;
}
return schemaDetails;
} else if (resolvedSchema.type === "array" && resolvedSchema.items) {
const arrayDetails = {
type: resolvedSchema.type,
items: processSchema(resolvedSchema.items)
};
if (resolvedSchema.minItems !== void 0) {
arrayDetails.minItems = resolvedSchema.minItems;
}
if (resolvedSchema.maxItems !== void 0) {
arrayDetails.maxItems = resolvedSchema.maxItems;
}
return arrayDetails;
}
return {
type: resolvedSchema.type
};
};
var resolveSchema = (context, schema2, { stack = 0, resolveFor = CONVERSION, seenRef = {}, isResponseSchema = false } = {}) => {
resetReadWritePropCache(context);
let resolvedSchema = _resolveSchema(context, schema2, stack, resolveFor, seenRef);
@@ -138227,12 +138365,16 @@ var require_schemaUtils2 = __commonJS({
return pmExamples;
};
var resolveBodyData = (context, requestBodySchema, bodyType, isExampleBody = false, responseCode = null, requestBodyExamples = {}) => {
let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = "", shouldGenerateFromExample = parametersResolution === "example", isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, bodyKey = isExampleBody ? "response" : "request", responseExamples, example, examples;
let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = "", shouldGenerateFromExample = parametersResolution === "example", isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, bodyKey = isExampleBody ? "response" : "request", responseExamples, example, examples, resolvedSchemaTypes = [];
if (_2.isEmpty(requestBodySchema)) {
return [{ [bodyKey]: bodyData }];
}
if (requestBodySchema.$ref) {
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
}
if (requestBodySchema.example !== void 0) {
const shouldResolveValueKey = _2.has(requestBodySchema.example, "value") && _2.keys(requestBodySchema.example).length <= 1;
@@ -138243,7 +138385,11 @@ var require_schemaUtils2 = __commonJS({
}
examples = requestBodySchema.examples || _2.get(requestBodySchema, "schema.examples");
requestBodySchema = requestBodySchema.schema || requestBodySchema;
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
if (example === void 0 && _2.get(requestBodySchema, "example") !== void 0) {
example = requestBodySchema.example;
}
@@ -138257,7 +138403,11 @@ var require_schemaUtils2 = __commonJS({
} else if (requestBodySchema) {
requestBodySchema = requestBodySchema.schema || requestBodySchema;
if (requestBodySchema.$ref) {
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
}
if (isBodyTypeXML) {
bodyData = xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution);
@@ -138285,6 +138435,10 @@ var require_schemaUtils2 = __commonJS({
}
}
}
if (context.enableTypeFetching && requestBodySchema.type !== void 0) {
const requestBodySchemaTypes = processSchema(requestBodySchema);
resolvedSchemaTypes.push(requestBodySchemaTypes);
}
if (isExampleBody && shouldGenerateFromExample && (_2.size(examples) > 1 || _2.size(requestBodyExamples) > 1)) {
responseExamples = [{
key: "_default",
@@ -138305,22 +138459,37 @@ var require_schemaUtils2 = __commonJS({
if (_2.isEmpty(matchedRequestBodyExamples)) {
matchedRequestBodyExamples = requestBodyExamples;
}
return generateExamples(context, responseExamples, matchedRequestBodyExamples, requestBodySchema, isBodyTypeXML);
const generatedBody = generateExamples(
context,
responseExamples,
matchedRequestBodyExamples,
requestBodySchema,
isBodyTypeXML
);
return {
generatedBody,
resolvedSchemaType: resolvedSchemaTypes[0]
};
}
return [{ [bodyKey]: bodyData }];
return {
generatedBody: [{ [bodyKey]: bodyData }],
resolvedSchemaType: resolvedSchemaTypes[0]
};
};
var resolveUrlEncodedRequestBodyForPostmanRequest = (context, requestBodyContent) => {
let bodyData = "", urlEncodedParams = [], requestBodyData = {
mode: "urlencoded",
urlencoded: urlEncodedParams
}, resolvedBody;
}, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
if (_2.isEmpty(requestBodyContent)) {
return requestBodyData;
}
if (_2.has(requestBodyContent, "schema.$ref")) {
requestBodyContent.schema = resolveSchema(context, requestBodyContent.schema);
}
resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
const encoding = requestBodyContent.encoding || {};
_2.forOwn(bodyData, (value, key) => {
@@ -138342,18 +138511,21 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: URLENCODED
}]
}],
resolvedSchemaTypeObject
};
};
var resolveFormDataRequestBodyForPostmanRequest = (context, requestBodyContent) => {
let bodyData = "", formDataParams = [], encoding = {}, requestBodyData = {
mode: "formdata",
formdata: formDataParams
}, resolvedBody;
}, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
if (_2.isEmpty(requestBodyContent)) {
return requestBodyData;
}
resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
encoding = _2.get(requestBodyContent, "encoding", {});
_2.forOwn(bodyData, (value, key) => {
@@ -138389,7 +138561,8 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: FORM_DATA
}]
}],
resolvedSchemaTypeObject
};
};
var getRawBodyType = (content) => {
@@ -138425,14 +138598,16 @@ var require_schemaUtils2 = __commonJS({
return bodyType;
};
var resolveRawModeRequestBodyForPostmanRequest = (context, requestContent) => {
let bodyType = getRawBodyType(requestContent), bodyData, headerFamily, dataToBeReturned = {}, { concreteUtils } = context, resolvedBody;
let bodyType = getRawBodyType(requestContent), bodyData, headerFamily, dataToBeReturned = {}, { concreteUtils } = context, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
headerFamily = getHeaderFamily(bodyType);
if (concreteUtils.isBinaryContentType(bodyType, requestContent)) {
dataToBeReturned = {
mode: "file"
};
} else {
resolvedBody = resolveBodyData(context, requestContent[bodyType], bodyType)[0];
resolvedBodyResult = resolveBodyData(context, requestContent[bodyType], bodyType);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
if (bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML) {
bodyData = getXmlVersionContent(bodyData);
@@ -138456,7 +138631,8 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: bodyType
}]
}],
resolvedSchemaTypeObject
};
};
var resolveRequestBodyForPostmanRequest = (context, operationItem) => {
@@ -138536,8 +138712,25 @@ var require_schemaUtils2 = __commonJS({
});
return reqParam;
};
var createProperties = (param) => {
const { schema: schema2 } = param;
return {
type: schema2.type,
format: schema2.format,
default: schema2.default,
required: param.required || false,
deprecated: param.deprecated || false,
enum: schema2.enum || void 0,
minLength: schema2.minLength,
maxLength: schema2.maxLength,
minimum: schema2.minimum,
maximum: schema2.maximum,
pattern: schema2.pattern,
example: schema2.example
};
};
var resolveQueryParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], { includeDeprecated } = context.computedOptions;
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], queryParamTypes = [], { includeDeprecated } = context.computedOptions;
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138545,20 +138738,28 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== QUERYPARAM || !includeDeprecated && param.deprecated) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let queryParamTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
queryParamTypeInfo = { keyName: param.name, properties };
queryParamTypes.push(queryParamTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { queryParamTypes, queryParams: pmParams };
};
var resolvePathParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [];
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], pathParamTypes = [];
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138566,17 +138767,25 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== PATHPARAM) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let pathParamTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
pathParamTypeInfo = { keyName: param.name, properties };
pathParamTypes.push(pathParamTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { pathParamTypes, pathParams: pmParams };
};
var resolveNameForPostmanReqeust = (context, operationItem, requestUrl) => {
let reqName, { requestNameSource } = context.computedOptions;
@@ -138598,7 +138807,7 @@ var require_schemaUtils2 = __commonJS({
return reqName;
};
var resolveHeadersForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], { keepImplicitHeaders, includeDeprecated } = context.computedOptions;
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], headerTypes = [], { keepImplicitHeaders, includeDeprecated } = context.computedOptions;
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138606,25 +138815,33 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== HEADER || !includeDeprecated && param.deprecated) {
return;
}
if (!keepImplicitHeaders && _2.includes(IMPLICIT_HEADERS, _2.toLower(_2.get(param, "name")))) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let headerTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
headerTypeInfo = { keyName: param.name, properties };
headerTypes.push(headerTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { headerTypes, headers: pmParams };
};
var resolveResponseBody = (context, responseBody = {}, requestBodyExamples = {}, code = null) => {
let responseContent, bodyType, allBodyData, headerFamily, acceptHeader, emptyResponse = [{
body: void 0
}];
}], resolvedResponseBodyResult, resolvedResponseBodyTypes;
if (_2.isEmpty(responseBody)) {
return emptyResponse;
}
@@ -138637,7 +138854,16 @@ var require_schemaUtils2 = __commonJS({
}
bodyType = getRawBodyType(responseContent);
headerFamily = getHeaderFamily(bodyType);
allBodyData = resolveBodyData(context, responseContent[bodyType], bodyType, true, code, requestBodyExamples);
resolvedResponseBodyResult = resolveBodyData(
context,
responseContent[bodyType],
bodyType,
true,
code,
requestBodyExamples
);
allBodyData = resolvedResponseBodyResult.generatedBody;
resolvedResponseBodyTypes = resolvedResponseBodyResult.resolvedSchemaType;
return _2.map(allBodyData, (bodyData) => {
let requestBodyData = bodyData.request, responseBodyData = bodyData.response, exampleName = bodyData.name;
if (bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML) {
@@ -138663,12 +138889,13 @@ var require_schemaUtils2 = __commonJS({
}],
name: exampleName,
bodyType,
acceptHeader
acceptHeader,
resolvedResponseBodyTypes
};
});
};
var resolveResponseHeaders = (context, responseHeaders) => {
const headers = [], { includeDeprecated } = context.computedOptions;
const headers = [], { includeDeprecated } = context.computedOptions, headerTypes = [];
if (_2.has(responseHeaders, "$ref")) {
responseHeaders = resolveSchema(context, responseHeaders, { isResponseSchema: true });
}
@@ -138679,14 +138906,33 @@ var require_schemaUtils2 = __commonJS({
if (!includeDeprecated && value.deprecated) {
return;
}
let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true });
let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true }), headerTypeInfo = {}, properties = {};
if (typeof headerValue === "number" || typeof headerValue === "boolean") {
headerValue = headerValue.toString();
}
const headerData = Object.assign({}, value, { name: headerName }), serialisedHeader = serialiseParamsBasedOnStyle(context, headerData, headerValue, { isResponseSchema: true });
headers.push(...serialisedHeader);
if (headerData && headerData.name && headerData.schema && headerData.schema.type) {
const { schema: schema2 } = headerData;
properties = {
type: schema2.type,
format: schema2.format,
default: schema2.default,
required: schema2.required || false,
deprecated: schema2.deprecated || false,
enum: schema2.enum || void 0,
minLength: schema2.minLength,
maxLength: schema2.maxLength,
minimum: schema2.minimum,
maximum: schema2.maximum,
pattern: schema2.pattern,
example: schema2.example
};
headerTypeInfo = { keyName: headerData.name, properties };
headerTypes.push(headerTypeInfo);
}
});
return headers;
return { resolvedHeaderTypes: headerTypes, headers };
};
var getPreviewLangugaForResponseBody = (bodyType) => {
const headerFamily = getHeaderFamily(bodyType);
@@ -138753,7 +138999,7 @@ var require_schemaUtils2 = __commonJS({
return responseAuthHelper;
};
var resolveResponseForPostmanRequest = (context, operationItem, request) => {
let responses = [], requestBodyExamples = [], requestAcceptHeader, requestBody = operationItem.requestBody, requestContent, rawBodyType, headerFamily, isBodyTypeXML;
let responses = [], requestBodyExamples = [], requestAcceptHeader, requestBody = operationItem.requestBody, requestContent, rawBodyType, headerFamily, isBodyTypeXML, resolvedExamplesObject = {}, responseTypes = {};
if (typeof requestBody === "object") {
if (requestBody.$ref) {
requestBody = resolveSchema(context, requestBody, { isResponseSchema: true });
@@ -138789,7 +139035,15 @@ var require_schemaUtils2 = __commonJS({
}
}
_2.forOwn(operationItem.responses, (responseObj, code) => {
let responseSchema = _2.has(responseObj, "$ref") ? resolveSchema(context, responseObj, { isResponseSchema: true }) : responseObj, { includeAuthInfoInExample } = context.computedOptions, auth = request.auth, resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples, code) || {}, headers = resolveResponseHeaders(context, responseSchema.headers);
let responseSchema = _2.has(responseObj, "$ref") ? resolveSchema(context, responseObj, { isResponseSchema: true }) : responseObj, { includeAuthInfoInExample } = context.computedOptions, auth = request.auth, resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples, code) || {}, { resolvedHeaderTypes, headers } = resolveResponseHeaders(context, responseSchema.headers), responseBodyHeaderObj;
resolvedExamplesObject = resolvedExamples[0] && resolvedExamples[0].resolvedResponseBodyTypes;
responseBodyHeaderObj = {
body: JSON.stringify(resolvedExamplesObject, null, 2),
headers: JSON.stringify(resolvedHeaderTypes, null, 2)
};
code = code.replace(/X|x/g, "0");
code = code === "default" ? 500 : _2.toSafeInteger(code);
Object.assign(responseTypes, { [code]: responseBodyHeaderObj });
_2.forOwn(resolvedExamples, (resolvedExample = {}) => {
let { body, contentHeader = [], bodyType, acceptHeader, name } = resolvedExample, resolvedRequestBody = _2.get(resolvedExample, "request.body"), originalRequest, response, responseAuthHelper, requestBodyObj = {}, reqHeaders = _2.clone(request.headers) || [], reqQueryParams = _2.clone(_2.get(request, "params.queryParams", []));
_2.isArray(acceptHeader) && reqHeaders.push(...acceptHeader);
@@ -138829,13 +139083,17 @@ var require_schemaUtils2 = __commonJS({
responses.push(response);
});
});
return { responses, acceptHeader: requestAcceptHeader };
return {
responses,
acceptHeader: requestAcceptHeader,
responseTypes
};
};
module2.exports = {
resolvePostmanRequest: function(context, operationItem, path, method) {
context.schemaCache = context.schemaCache || {};
context.schemaFakerCache = context.schemaFakerCache || {};
let url = resolveUrlForPostmanRequest(path), baseUrlData = resolveBaseUrlForPostmanRequest(operationItem[method]), requestName = resolveNameForPostmanReqeust(context, operationItem[method], url), queryParams = resolveQueryParamsForPostmanRequest(context, operationItem, method), headers = resolveHeadersForPostmanRequest(context, operationItem, method), pathParams = resolvePathParamsForPostmanRequest(context, operationItem, method), { pathVariables, collectionVariables } = filterCollectionAndPathVariables(url, pathParams), requestBody = resolveRequestBodyForPostmanRequest(context, operationItem[method]), request, securitySchema = _2.get(operationItem, [method, "security"]), authHelper = generateAuthForCollectionFromOpenAPI(context.openapi, securitySchema), { alwaysInheritAuthentication } = context.computedOptions;
let url = resolveUrlForPostmanRequest(path), baseUrlData = resolveBaseUrlForPostmanRequest(operationItem[method]), requestName = resolveNameForPostmanReqeust(context, operationItem[method], url), { queryParamTypes, queryParams } = resolveQueryParamsForPostmanRequest(context, operationItem, method), { headerTypes, headers } = resolveHeadersForPostmanRequest(context, operationItem, method), { pathParamTypes, pathParams } = resolvePathParamsForPostmanRequest(context, operationItem, method), { pathVariables, collectionVariables } = filterCollectionAndPathVariables(url, pathParams), requestBody = resolveRequestBodyForPostmanRequest(context, operationItem[method]), requestBodyTypes = requestBody && requestBody.resolvedSchemaTypeObject, request, securitySchema = _2.get(operationItem, [method, "security"]), authHelper = generateAuthForCollectionFromOpenAPI(context.openapi, securitySchema), { alwaysInheritAuthentication } = context.computedOptions, requestIdentifier, requestTypesObject = {};
headers.push(..._2.get(requestBody, "headers", []));
pathVariables.push(...baseUrlData.pathVariables);
collectionVariables.push(...baseUrlData.collectionVariables);
@@ -138853,7 +139111,21 @@ var require_schemaUtils2 = __commonJS({
body: _2.get(requestBody, "body"),
auth: alwaysInheritAuthentication ? void 0 : authHelper
};
const { responses, acceptHeader } = resolveResponseForPostmanRequest(context, operationItem[method], request);
const requestTypes = {
body: JSON.stringify(requestBodyTypes, null, 2),
headers: JSON.stringify(headerTypes, null, 2),
pathParam: JSON.stringify(pathParamTypes, null, 2),
queryParam: JSON.stringify(queryParamTypes, null, 2)
}, {
responses,
acceptHeader,
responseTypes
} = resolveResponseForPostmanRequest(context, operationItem[method], request);
requestIdentifier = method + path;
Object.assign(
requestTypesObject,
{ [requestIdentifier]: { request: requestTypes, response: responseTypes } }
);
if (!_2.isEmpty(acceptHeader)) {
request.headers = _2.concat(request.headers, acceptHeader);
}
@@ -138864,7 +139136,8 @@ var require_schemaUtils2 = __commonJS({
responses
})
},
collectionVariables
collectionVariables,
requestTypesObject
};
},
resolveResponseForPostmanRequest,
@@ -141220,7 +141493,7 @@ var require_libV2 = __commonJS({
convertV2: function(context, cb) {
let collectionTree = generateSkeletonTreeFromOpenAPI(context.openapi, context.computedOptions);
let preOrderTraversal = GraphLib.alg.preorder(collectionTree, "root:collection");
let collection = {};
let collection = {}, extractedTypesObject = {};
_2.forEach(preOrderTraversal, function(nodeIdentified) {
let node = collectionTree.node(nodeIdentified);
switch (node.type) {
@@ -141254,15 +141527,16 @@ var require_libV2 = __commonJS({
break;
}
case "request": {
let request = {}, collectionVariables = [], requestObject = {};
let request = {}, collectionVariables = [], requestObject = {}, requestTypesObject = {};
try {
({ request, collectionVariables } = resolvePostmanRequest(
({ request, collectionVariables, requestTypesObject } = resolvePostmanRequest(
context,
context.openapi.paths[node.meta.path],
node.meta.path,
node.meta.method
));
requestObject = generateRequestItemObject(request);
extractedTypesObject = Object.assign({}, extractedTypesObject, requestTypesObject);
} catch (error) {
console.error(error);
break;
@@ -141337,6 +141611,17 @@ var require_libV2 = __commonJS({
if (!_2.isEmpty(collection.variable)) {
collection.variable = _2.uniqBy(collection.variable, "key");
}
if (context.enableTypeFetching) {
return cb(null, {
result: true,
output: [{
type: "collection",
data: collection
}],
analytics: this.analytics || {},
extractedTypes: extractedTypesObject || {}
});
}
return cb(null, {
result: true,
output: [{
@@ -145118,7 +145403,7 @@ var require_schemapack = __commonJS({
var concreteUtils;
var pathBrowserify = require_path_browserify();
var SchemaPack = class {
constructor(input, options = {}, moduleVersion = MODULE_VERSION.V1) {
constructor(input, options = {}, moduleVersion = MODULE_VERSION.V1, enableTypeFetching = false) {
if (input.type === schemaUtils.MULTI_FILE_API_TYPE_ALLOWED_VALUE && input.data && input.data[0] && input.data[0].path) {
input = schemaUtils.mapDetectRootFilesInputToFolderInput(input);
}
@@ -145136,6 +145421,7 @@ var require_schemapack = __commonJS({
actualStack: 0,
numberOfRequests: 0
};
this.enableTypeFetching = enableTypeFetching;
this.computedOptions = utils.mergeOptions(
// predefined options
_2.keyBy(this.definedOptions, "id"),
@@ -145814,6 +146100,14 @@ var require_openapi_to_postmanv2 = __commonJS({
}
return cb(new UserError(_2.get(schema2, "validationResult.reason", DEFAULT_INVALID_ERROR)));
},
convertV2WithTypes: function(input, options, cb) {
const enableTypeFetching = true;
var schema2 = new SchemaPack(input, options, MODULE_VERSION.V2, enableTypeFetching);
if (schema2.validated) {
return schema2.convertV2(cb);
}
return cb(new UserError(_2.get(schema2, "validationResult.reason", DEFAULT_INVALID_ERROR)));
},
validate: function(input) {
var schema2 = new SchemaPack(input);
return schema2.validationResult;

View File

@@ -7,7 +7,7 @@
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"openapi-to-postmanv2": "^4.23.1",
"openapi-to-postmanv2": "^5.0.0",
"yaml": "^2.4.2"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./gen_models";
import type { SyncModel } from "./gen_models.js";
export type GitAuthor = { name: string | null, email: string | null, };

View File

@@ -51,6 +51,14 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|commit', { dir, ...args }),
onSuccess,
}),
commitAndPush: useMutation<PushResult, string, { message: string }>({
mutationKey: ['git', 'commitpush', dir],
mutationFn: async (args) => {
await invoke('plugin:yaak-git|commit', { dir, ...args });
return invoke('plugin:yaak-git|push', { dir });
},
onSuccess,
}),
fetchAll: useMutation<string, string, void>({
mutationKey: ['git', 'checkout', dir],
mutationFn: () => invoke('plugin:yaak-git|fetch_all', { dir }),

View File

@@ -117,9 +117,9 @@ pub fn git_commit(dir: &Path, message: &str) -> Result<()> {
let tree = repo.find_tree(tree_oid)?;
// Make the signature
let config = git2::Config::open_default()?.snapshot()?;
let name = config.get_str("user.name").unwrap_or("Change Me");
let email = config.get_str("user.email").unwrap_or("change_me@example.com");
let config = repo.config()?.snapshot()?;
let name = config.get_str("user.name").unwrap_or("Unknown");
let email = config.get_str("user.email")?;
let sig = git2::Signature::now(name, email)?;
// Get the current HEAD commit (if it exists)

View File

@@ -8,7 +8,7 @@ use tauri::{
mod branch;
mod callbacks;
mod commands;
mod error;
pub mod error;
mod fetch;
mod git;
mod merge;

View File

@@ -28,7 +28,7 @@ pub async fn fill_pool_from_files(
let desc_path = temp_dir().join(random_file_name);
let global_import_dir = app_handle
.path()
.resolve("vendored/protoc/protoc-include", BaseDirectory::Resource)
.resolve("vendored/protoc/include", BaseDirectory::Resource)
.expect("failed to resolve protoc include directory");
// HACK: Remove UNC prefix for Windows paths

View File

@@ -6,15 +6,15 @@ edition = "2021"
publish = false
[dependencies]
chrono = "0.4.38"
log = "0.4.26"
reqwest = { workspace = true, features = ["json"] }
serde = { version = "1.0.208", features = ["derive"] }
ts-rs = { workspace = true }
thiserror = { workspace = true }
tauri = { workspace = true }
yaak-models = { workspace = true }
chrono = "0.4.38"
log = "0.4.22"
serde_json = "1.0.132"
tauri = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
yaak-models = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CheckActivationRequestPayload = { activationId: string, };
export type CheckActivationRequestPayload = { appVersion: string, appPlatform: string, };

View File

@@ -8,4 +8,6 @@ export type ActivateLicenseResponsePayload = { activationId: string, };
export type CheckActivationResponsePayload = { active: boolean, };
export type DeactivateLicenseRequestPayload = { appVersion: string, appPlatform: string, };
export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, };

View File

@@ -1,4 +1,4 @@
const COMMANDS: &[&str] = &["activate", "check"];
const COMMANDS: &[&str] = &["activate", "deactivate", "check"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();

View File

@@ -14,6 +14,12 @@ export function useLicense() {
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
const deactivate = useMutation<void, string, void>({
mutationKey: ['license.deactivate'],
mutationFn: () => invoke('plugin:yaak-license|deactivate'),
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
// Check the license again after a license is activated
useEffect(() => {
const unlisten = listen('license-activated', async () => {
@@ -26,12 +32,15 @@ export function useLicense() {
const CHECK_QUERY_KEY = ['license.check'];
const check = useQuery<void, string, LicenseCheckStatus>({
refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours
refetchOnWindowFocus: false,
queryKey: CHECK_QUERY_KEY,
queryFn: () => invoke('plugin:yaak-license|check'),
});
return {
activate,
deactivate,
check,
} as const;
}

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-deactivate"
description = "Enables the deactivate command without any pre-configured scope."
commands.allow = ["deactivate"]
[[permission]]
identifier = "deny-deactivate"
description = "Denies the deactivate command without any pre-configured scope."
commands.deny = ["deactivate"]

View File

@@ -4,6 +4,7 @@ Default permissions for the plugin
- `allow-check`
- `allow-activate`
- `allow-deactivate`
## Permission Table
@@ -63,6 +64,32 @@ Enables the check command without any pre-configured scope.
Denies the check command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:allow-deactivate`
</td>
<td>
Enables the deactivate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-deactivate`
</td>
<td>
Denies the deactivate command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -1,3 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-check", "allow-activate"]
permissions = ["allow-check", "allow-activate", "allow-deactivate"]

View File

@@ -314,6 +314,16 @@
"type": "string",
"const": "deny-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "allow-deactivate"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -1,5 +1,8 @@
use crate::errors::Result;
use crate::{activate_license, check_license, ActivateLicenseRequestPayload, LicenseCheckStatus};
use crate::error::Result;
use crate::{
activate_license, check_license, deactivate_license, ActivateLicenseRequestPayload,
CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus,
};
use log::{debug, info};
use std::string::ToString;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
@@ -7,7 +10,14 @@ use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
#[command]
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license");
check_license(&app_handle).await
check_license(
&app_handle,
CheckActivationRequestPayload {
app_platform: get_os().to_string(),
app_version: app_handle.package_info().version.to_string(),
},
)
.await
}
#[command]
@@ -24,6 +34,19 @@ pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -
.await
}
#[command]
pub async fn deactivate<R: Runtime>(window: WebviewWindow<R>) -> Result<()> {
info!("Deactivating activation");
deactivate_license(
&window,
DeactivateLicenseRequestPayload {
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
},
)
.await
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"

View File

@@ -5,12 +5,14 @@ use tauri::{
};
mod commands;
mod errors;
pub mod error;
mod license;
use crate::commands::{activate, check};
use crate::commands::{activate, check, deactivate};
pub use license::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-license").invoke_handler(generate_handler![check, activate]).build()
Builder::new("yaak-license")
.invoke_handler(generate_handler![check, activate, deactivate])
.build()
}

View File

@@ -1,11 +1,11 @@
use crate::errors::Error::{ClientError, ServerError};
use crate::errors::Result;
use crate::error::Error::{ClientError, ServerError};
use crate::error::Result;
use chrono::{NaiveDateTime, Utc};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::ops::Add;
use std::time::Duration;
use tauri::{is_dev, AppHandle, Emitter, Runtime, WebviewWindow};
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::queries::UpdateSource;
@@ -17,7 +17,8 @@ const TRIAL_SECONDS: u64 = 3600 * 24 * 30;
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct CheckActivationRequestPayload {
pub activation_id: String,
pub app_version: String,
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
@@ -36,6 +37,14 @@ pub struct ActivateLicenseRequestPayload {
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct DeactivateLicenseRequestPayload {
pub app_version: String,
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
@@ -56,7 +65,7 @@ pub async fn activate_license<R: Runtime>(
p: ActivateLicenseRequestPayload,
) -> Result<()> {
let client = reqwest::Client::new();
let response = client.post(build_url("/activate")).json(&p).send().await?;
let response = client.post(build_url("/licenses/activate")).json(&p).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -86,6 +95,44 @@ pub async fn activate_license<R: Runtime>(
Ok(())
}
pub async fn deactivate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: DeactivateLicenseRequestPayload,
) -> Result<()> {
let activation_id = get_activation_id(window).await;
let client = reqwest::Client::new();
let path = format!("/licenses/activations/{}/deactivate", activation_id);
let response = client.post(build_url(&path)).json(&p).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
return Err(ClientError {
message: body.message,
error: body.error,
});
}
if response.status().is_server_error() {
return Err(ServerError);
}
yaak_models::queries::delete_key_value(
window,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
&UpdateSource::Window,
)
.await;
if let Err(e) = window.emit("license-deactivated", true) {
warn!("Failed to emit deactivate-license event: {}", e);
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "license.ts")]
@@ -96,15 +143,8 @@ pub enum LicenseCheckStatus {
Trialing { end: NaiveDateTime },
}
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> {
let activation_id = yaak_models::queries::get_key_value_string(
app_handle,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
"",
)
.await;
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>, payload: CheckActivationRequestPayload) -> Result<LicenseCheckStatus> {
let activation_id = get_activation_id(app_handle).await;
let settings = yaak_models::queries::get_or_create_settings(app_handle).await;
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
@@ -122,10 +162,8 @@ pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Lice
info!("Checking license activation");
// A license has been activated, so let's check the license server
let client = reqwest::Client::new();
let payload = CheckActivationRequestPayload {
activation_id: activation_id.clone(),
};
let response = client.post(build_url("/check")).json(&payload).send().await?;
let path = format!("/licenses/activations/{activation_id}/check");
let response = client.post(build_url(&path)).json(&payload).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -151,8 +189,13 @@ pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Lice
fn build_url(path: &str) -> String {
if is_dev() {
format!("http://localhost:9444/licenses{path}")
format!("http://localhost:9444{path}")
} else {
format!("https://license.yaak.app/licenses{path}")
format!("https://license.yaak.app{path}")
}
}
pub async fn get_activation_id<R: Runtime>(mgr: &impl Manager<R>) -> String {
yaak_models::queries::get_key_value_string(mgr, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
.await
}

View File

@@ -54,7 +54,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type SyncHistory = { model: "sync_history", id: string, workspaceId: string, createdAt: string, states: Array<SyncState>, checksum: string, relPath: string, syncDir: string, };
@@ -64,11 +64,11 @@ export type UpdateSource = "sync" | "window" | "plugin" | "background" | "import
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
export type WebsocketConnectionState = "initialized" | "connected" | "closed";
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
export type WebsocketEventType = "binary" | "close" | "frame" | "ping" | "pong" | "text";
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketMessageType = "text" | "binary";

View File

@@ -87,7 +87,6 @@ pub struct Settings {
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
pub proxy: Option<ProxySetting>,
pub telemetry: bool,
pub theme: String,
pub theme_dark: String,
pub theme_light: String,
@@ -112,7 +111,6 @@ pub enum SettingsIden {
InterfaceScale,
OpenWorkspaceNewWindow,
Proxy,
Telemetry,
Theme,
ThemeDark,
ThemeLight,
@@ -138,7 +136,6 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
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")?,
@@ -552,6 +549,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpRequest {
pub enum WebsocketConnectionState {
Initialized,
Connected,
Closing,
Closed,
}
@@ -717,6 +715,7 @@ pub enum WebsocketEventType {
Binary,
Close,
Frame,
Open,
Ping,
Pong,
Text,

View File

@@ -6,9 +6,9 @@ use crate::models::{
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
PluginKeyValue, PluginKeyValueIden, Settings, SettingsIden, SyncState, SyncStateIden,
WebsocketConnection, WebsocketConnectionIden, WebsocketEvent, WebsocketEventIden,
WebsocketRequest, WebsocketRequestIden, Workspace, WorkspaceIden, WorkspaceMeta,
WorkspaceMetaIden,
WebsocketConnection, WebsocketConnectionIden, WebsocketConnectionState, WebsocketEvent,
WebsocketEventIden, WebsocketRequest, WebsocketRequestIden, Workspace, WorkspaceIden,
WorkspaceMeta, WorkspaceMetaIden,
};
use crate::plugin::SqliteConnection;
use chrono::{NaiveDateTime, Utc};
@@ -134,6 +134,31 @@ pub async fn set_key_value_raw<R: Runtime>(
(m, existing.is_none())
}
pub async fn delete_key_value<R: Runtime>(
w: &WebviewWindow<R>,
namespace: &str,
key: &str,
update_source: &UpdateSource,
) {
let kv = match get_key_value_raw(w, namespace, key).await {
None => return,
Some(m) => m,
};
let dbm = &*w.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::delete()
.from_table(KeyValueIden::Table)
.cond_where(
Cond::all()
.add(Expr::col(KeyValueIden::Namespace).eq(namespace))
.add(Expr::col(KeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params()).expect("Failed to delete PluginKeyValue");
emit_deleted_model(w, &AnyModel::KeyValue(kv.to_owned()), update_source);
}
pub async fn list_key_values_raw<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<KeyValue>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1478,7 +1503,6 @@ pub async fn update_settings<R: Runtime>(
(SettingsIden::EditorFontSize, settings.editor_font_size.into()),
(SettingsIden::EditorKeymap, settings.editor_keymap.to_string().into()),
(SettingsIden::EditorSoftWrap, settings.editor_soft_wrap.into()),
(SettingsIden::Telemetry, settings.telemetry.into()),
(SettingsIden::OpenWorkspaceNewWindow, settings.open_workspace_new_window.into()),
(
SettingsIden::Proxy,
@@ -2119,6 +2143,21 @@ pub async fn create_http_response<R: Runtime>(
Ok(m)
}
pub async fn cancel_pending_websocket_connections<R: Runtime>(mgr: &impl Manager<R>) -> Result<()> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let closed = serde_json::to_value(&WebsocketConnectionState::Closed)?;
let (sql, params) = Query::update()
.table(WebsocketConnectionIden::Table)
.values([(WebsocketConnectionIden::State, closed.as_str().into())])
.cond_where(Expr::col(WebsocketConnectionIden::State).ne(closed.as_str()))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
stmt.execute(&*params.as_params())?;
Ok(())
}
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();
@@ -2134,7 +2173,7 @@ pub async fn cancel_pending_grpc_connections(app: &AppHandle) -> Result<()> {
Ok(())
}
pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> {
pub async fn cancel_pending_http_responses(app: &AppHandle) -> Result<()> {
let dbm = &*app.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();

View File

@@ -346,7 +346,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
@@ -354,7 +354,7 @@ export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**

View File

@@ -3,7 +3,9 @@ use std::collections::HashMap;
use tauri::{Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::models::{Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace};
use yaak_models::models::{
Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace,
};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -108,6 +110,7 @@ pub enum InternalEventPayload {
OpenWindowRequest(OpenWindowRequest),
WindowNavigateEvent(WindowNavigateEvent),
WindowCloseEvent,
CloseWindowRequest(CloseWindowRequest),
TemplateRenderRequest(TemplateRenderRequest),
@@ -262,10 +265,15 @@ pub struct OpenWindowRequest {
pub url: String,
/// Label for the window. If not provided, a random one will be generated.
pub label: String,
#[ts(optional)]
pub title: Option<String>,
#[ts(optional)]
pub size: Option<WindowSize>,
#[ts(optional)]
pub data_dir_key: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]

View File

@@ -27,6 +27,7 @@ use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout;
use yaak_models::queries::{generate_id, list_plugins};
use yaak_templates::error::Error::RenderError;
#[derive(Clone)]
pub struct PluginManager {
@@ -74,13 +75,14 @@ impl PluginManager {
// Handle when client plugin runtime disconnects
tauri::async_runtime::spawn(async move {
while let Some(_) = client_disconnect_rx.recv().await {
info!("Plugin runtime client disconnected! TODO: Handle this case");
// Happens when the app is closed
info!("Plugin runtime client disconnected");
}
});
let listen_addr = match option_env!("YAAK_PLUGIN_SERVER_PORT") {
Some(port) => format!("localhost:{port}"),
None => "localhost:0".to_string(),
Some(port) => format!("127.0.0.1:{port}"),
None => "127.0.0.1:0".to_string(),
};
let listener = tauri::async_runtime::block_on(async move {
TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener")
@@ -206,7 +208,7 @@ impl PluginManager {
// Boot the plugin
let event = timeout(
Duration::from_secs(1),
Duration::from_secs(2),
self.send_to_plugin_and_wait(
window_context,
&plugin_handle,
@@ -559,7 +561,7 @@ impl PluginManager {
Some(JsonPrimitive::Boolean(v)) => v.clone(),
_ => false,
};
// Auth is disabled, so don't do anything
if disabled {
info!("Not applying disabled auth {:?}", auth_name);
@@ -596,7 +598,7 @@ impl PluginManager {
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
) -> Result<Option<String>> {
) -> yaak_templates::error::Result<String> {
let req = CallTemplateFunctionRequest {
name: fn_name.to_string(),
args: CallTemplateFunctionArgs {
@@ -607,7 +609,8 @@ impl PluginManager {
let events = self
.send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req))
.await?;
.await
.map_err(|e| RenderError(format!("Failed to call template function {e:}")))?;
let value = events.into_iter().find_map(|e| match e.payload {
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
@@ -616,14 +619,17 @@ impl PluginManager {
_ => None,
});
Ok(value)
match value {
None => Err(RenderError(format!("Template function not found {fn_name}"))),
Some(v) => Ok(v),
}
}
pub async fn import_data<R: Runtime>(
&self,
window: &WebviewWindow<R>,
content: &str,
) -> Result<(ImportResponse, String)> {
) -> Result<ImportResponse> {
let reply_events = self
.send_and_wait(
&WindowContext::from_window(window),
@@ -635,19 +641,13 @@ impl PluginManager {
// TODO: Don't just return the first valid response
let result = reply_events.into_iter().find_map(|e| match e.payload {
InternalEventPayload::ImportResponse(resp) => Some((resp, e.plugin_ref_id)),
InternalEventPayload::ImportResponse(resp) => Some(resp),
_ => None,
});
match result {
None => Err(PluginErr("No importers found for file contents".to_string())),
Some((resp, ref_id)) => {
let plugin = self
.get_plugin_by_ref_id(ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(ref_id))?;
Ok((resp, plugin.info().await.name))
}
Some(resp) => Ok(resp),
}
}

View File

@@ -1,7 +1,8 @@
use crate::events::{FormInput, RenderPurpose, WindowContext};
use crate::events::{RenderPurpose, WindowContext};
use crate::manager::PluginManager;
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use yaak_templates::error::Result;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
@@ -27,51 +28,20 @@ impl PluginTemplateCallback {
}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self
.plugin_manager
.get_template_functions_with_context(&self.window_context)
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for arg in function.args {
let base = match arg {
FormInput::Text(a) => a.base,
FormInput::Editor(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
FormInput::Accordion(_) => continue,
FormInput::Banner(_) => continue,
FormInput::Markdown(_) => continue,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = self
.plugin_manager
.call_template_function(
&self.window_context,
fn_name,
args_with_defaults,
args,
self.render_purpose.to_owned(),
)
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())
.await?;
Ok(resp)
}
}

View File

@@ -77,5 +77,7 @@ function removeWatchKey(key: string) {
// On page load, unlisten to all zombie watchers
const keys = getWatchKeys();
console.log('Unsubscribing to zombie file watchers', keys);
keys.forEach(unlistenToWatcher);
if (keys.length > 0) {
console.log('Unsubscribing to zombie file watchers', keys);
keys.forEach(unlistenToWatcher);
}

View File

@@ -1,7 +1,7 @@
use crate::error::Result;
use crate::models::SyncModel;
use chrono::Utc;
use log::{debug, warn};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
@@ -164,6 +164,13 @@ pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
let path = dir_entry.path();
let (model, checksum) = match SyncModel::from_file(&path) {
// TODO: Remove this once we have logic to handle environments. This it to clean
// any existing ones from the sync dir that resulted from the 2025.1 betas.
Ok(Some((SyncModel::Environment(e), _))) => {
fs::remove_file(path).await?;
info!("Cleaned up synced environment {}", e.id);
continue;
}
Ok(Some(m)) => m,
Ok(None) => continue,
Err(e) => {
@@ -205,9 +212,17 @@ pub(crate) fn compute_sync_ops(
let op = match (db_map.get(k), fs_map.get(k)) {
(None, None) => return None, // Can never happen
(None, Some(fs)) => SyncOp::DbCreate { fs: fs.to_owned() },
(Some(DbCandidate::Unmodified(model, sync_state)), None) => SyncOp::DbDelete {
model: model.to_owned(),
state: sync_state.to_owned(),
(Some(DbCandidate::Unmodified(model, sync_state)), None) => {
// TODO: Remove this once we have logic to handle environments. This it to
// ignore the cleaning we did above of any environments that were written
// to disk in the 2025.1 betas.
if let SyncModel::Environment(_) = model {
return None;
}
SyncOp::DbDelete {
model: model.to_owned(),
state: sync_state.to_owned(),
}
},
(Some(DbCandidate::Modified(model, sync_state)), None) => SyncOp::FsUpdate {
model: model.to_owned(),
@@ -318,7 +333,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
);
let mut sync_state_ops = Vec::new();
let mut workspaces_to_upsert = Vec::new();
let mut environments_to_upsert = Vec::new();
let environments_to_upsert = Vec::new();
let mut folders_to_upsert = Vec::new();
let mut http_requests_to_upsert = Vec::new();
let mut grpc_requests_to_upsert = Vec::new();
@@ -380,11 +395,13 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
// batch upsert to make foreign keys happy
match fs.model {
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
SyncModel::Environment(m) => environments_to_upsert.push(m),
SyncModel::Folder(m) => folders_to_upsert.push(m),
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
// TODO: Handle environments in sync
SyncModel::Environment(_) => {}
};
SyncStateOp::Create {
model_id,
@@ -397,11 +414,13 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
// batch upsert to make foreign keys happy
match fs.model {
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
SyncModel::Environment(m) => environments_to_upsert.push(m),
SyncModel::Folder(m) => folders_to_upsert.push(m),
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
// TODO: Handle environments in sync
SyncModel::Environment(_) => {}
}
SyncStateOp::Update {
state: state.to_owned(),

View File

@@ -5,8 +5,10 @@ edition = "2021"
publish = false
[dependencies]
base64 = "0.22.1"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] }
ts-rs = { version = "10.0.0" }
thiserror = { workspace = true }
tokio = { version = "1.39.3", features = ["macros", "rt"] }
serde_json = "1.0.132"
ts-rs = { version = "10.0.0" }

View File

@@ -0,0 +1,25 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum Error {
#[error("Render Error: {0}")]
RenderError(String),
#[error("Render Error: Variable \"{0}\" is not defined in active environment")]
VariableNotFound(String),
#[error("Render Error: Max recursion depth exceeded")]
RenderStackExceededError,
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

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

View File

@@ -1,3 +1,7 @@
use crate::error::Error::RenderError;
use crate::error::Result;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use ts_rs::TS;
@@ -43,7 +47,13 @@ pub enum Val {
impl Display for Val {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Val::Str { text } => format!("'{}'", text.to_string().replace("'", "\'")),
Val::Str { text } => {
if text.chars().all(|c| c.is_alphanumeric() || c == ' ' || c == '_' || c == '_') {
format!("'{}'", text)
} else {
format!("b64'{}'", BASE64_URL_SAFE_NO_PAD.encode(text))
}
}
Val::Var { name } => name.to_string(),
Val::Bool { value } => value.to_string(),
Val::Fn { name, args } => {
@@ -108,13 +118,13 @@ impl Parser {
}
}
pub fn parse(&mut self) -> Tokens {
pub fn parse(&mut self) -> Result<Tokens> {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag() {
if let Some(t) = self.parse_tag()? {
self.push_token(t);
} else {
self.pos = start_curr;
@@ -131,29 +141,29 @@ impl Parser {
}
self.push_token(Token::Eof);
Tokens {
Ok(Tokens {
tokens: self.tokens.clone(),
}
})
}
fn parse_tag(&mut self) -> Option<Token> {
fn parse_tag(&mut self) -> Result<Option<Token>> {
// Parse up to first identifier
// ${[ my_var...
self.skip_whitespace();
let val = match self.parse_value() {
let val = match self.parse_value()? {
Some(v) => v,
None => return None,
None => return Ok(None),
};
// Parse to closing tag
// ${[ my_var(a, b, c) ]}
self.skip_whitespace();
if !self.match_str("]}") {
return None;
return Ok(None);
}
Some(Token::Tag { val })
Ok(Some(Token::Tag { val }))
}
#[allow(dead_code)]
@@ -167,9 +177,11 @@ impl Parser {
);
}
fn parse_value(&mut self) -> Option<Val> {
if let Some((name, args)) = self.parse_fn() {
fn parse_value(&mut self) -> Result<Option<Val>> {
let v = if let Some((name, args)) = self.parse_fn()? {
Some(Val::Fn { name, args })
} else if let Some(v) = self.parse_string()? {
Some(Val::Str { text: v })
} else if let Some(v) = self.parse_ident() {
if v == "null" {
Some(Val::Null)
@@ -180,38 +192,38 @@ impl Parser {
} else {
Some(Val::Var { name: v })
}
} else if let Some(v) = self.parse_string() {
Some(Val::Str { text: v })
} else {
None
}
};
Ok(v)
}
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
fn parse_fn(&mut self) -> Result<Option<(String, Vec<FnArg>)>> {
let start_pos = self.pos;
let name = match self.parse_fn_name() {
Some(v) => v,
None => {
self.pos = start_pos;
return None;
return Ok(None);
}
};
let args = match self.parse_fn_args() {
let args = match self.parse_fn_args()? {
Some(args) => args,
None => {
self.pos = start_pos;
return None;
return Ok(None);
}
};
Some((name, args))
Ok(Some((name, args)))
}
fn parse_fn_args(&mut self) -> Option<Vec<FnArg>> {
fn parse_fn_args(&mut self) -> Result<Option<Vec<FnArg>>> {
if !self.match_str("(") {
return None;
return Ok(None);
}
let start_pos = self.pos;
@@ -221,7 +233,7 @@ impl Parser {
// Fn closed immediately
self.skip_whitespace();
if self.match_str(")") {
return Some(args);
return Ok(Some(args));
}
while self.pos < self.chars.len() {
@@ -231,7 +243,7 @@ impl Parser {
self.skip_whitespace();
self.match_str("=");
self.skip_whitespace();
let value = self.parse_value();
let value = self.parse_value()?;
self.skip_whitespace();
if let (Some(name), Some(value)) = (name.clone(), value.clone()) {
@@ -239,7 +251,7 @@ impl Parser {
} else {
// Didn't find valid thing, so return
self.pos = start_pos;
return None;
return Ok(None);
}
if self.match_str(")") {
@@ -251,7 +263,7 @@ impl Parser {
// If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None;
return Ok(None);
}
if start_pos == self.pos {
@@ -259,7 +271,7 @@ impl Parser {
}
}
Some(args)
Ok(Some(args))
}
fn parse_ident(&mut self) -> Option<String> {
@@ -269,9 +281,9 @@ impl Parser {
while self.pos < self.chars.len() {
let ch = self.peek_char();
let is_valid = if start_pos == self.pos {
ch.is_alphabetic() // First char has to be alphabetic
ch.is_alphabetic() || ch == '_' // First is more restrictive
} else {
ch.is_alphanumeric() || ch == '-' || ch == '_'
ch.is_alphanumeric() || ch == '_' || ch == '-'
};
if is_valid {
text.push(ch);
@@ -319,12 +331,17 @@ impl Parser {
Some(text)
}
fn parse_string(&mut self) -> Option<String> {
fn parse_string(&mut self) -> Result<Option<String>> {
let start_pos = self.pos;
let mut text = String::new();
if !self.match_str("'") {
return None;
let mut is_b64 = false;
if self.match_str("b64'") {
is_b64 = true;
} else if self.match_str("'") {
// Nothing
} else {
return Ok(None);
}
let mut found_closing = false;
@@ -350,10 +367,21 @@ impl Parser {
if !found_closing {
self.pos = start_pos;
return None;
return Ok(None);
}
Some(text)
let final_text = if is_b64 {
let decoded = BASE64_URL_SAFE_NO_PAD
.decode(text.clone())
.map_err(|_| RenderError(format!("Failed to decode string {text}")))?;
let decoded = String::from_utf8(decoded)
.map_err(|_| RenderError(format!("Failed to decode utf8 string {text}")))?;
decoded
} else {
text
};
Ok(Some(final_text))
}
fn skip_whitespace(&mut self) {
@@ -410,14 +438,15 @@ impl Parser {
#[cfg(test)]
mod tests {
use crate::error::Result;
use crate::Val::Null;
use crate::*;
#[test]
fn var_simple() {
fn var_simple() -> Result<()> {
let mut p = Parser::new("${[ foo ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Var { name: "foo".into() }
@@ -425,13 +454,14 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn var_dashes() {
fn var_dashes() -> Result<()> {
let mut p = Parser::new("${[ a-b ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Var { name: "a-b".into() }
@@ -439,13 +469,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn var_underscores() {
fn var_underscores() -> Result<()> {
let mut p = Parser::new("${[ a_b ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Var { name: "a_b".into() }
@@ -453,42 +485,48 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn var_prefixes() {
let mut p = Parser::new("${[ -a ]}${[ _a ]}${[ 0a ]}");
fn var_prefixes() -> Result<()> {
let mut p = Parser::new("${[ -a ]}${[ 0a ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Raw {
// Shouldn't be parsed, because they're invalid
text: "${[ -a ]}${[ _a ]}${[ 0a ]}".into()
text: "${[ -a ]}${[ 0a ]}".into()
},
Token::Eof
]
);
Ok(())
}
#[test]
fn var_underscore_prefix() {
fn var_underscore_prefix() -> Result<()> {
let mut p = Parser::new("${[ _a ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Raw {
text: "${[ _a ]}".into()
Token::Tag {
val: Val::Var { name: "_a".into() }
},
Token::Eof
]
);
Ok(())
}
#[test]
fn var_boolean() {
fn var_boolean() -> Result<()> {
let mut p = Parser::new("${[ true ]}${[ false ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Bool { value: true },
@@ -499,13 +537,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn var_multiple_names_invalid() {
fn var_multiple_names_invalid() -> Result<()> {
let mut p = Parser::new("${[ foo bar ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Raw {
text: "${[ foo bar ]}".into()
@@ -513,13 +553,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn tag_string() {
fn tag_string() -> Result<()> {
let mut p = Parser::new(r#"${[ 'foo \'bar\' baz' ]}"#);
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Str {
@@ -529,13 +571,33 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn var_surrounded() {
fn tag_b64_string() -> Result<()> {
let mut p = Parser::new(r#"${[ b64'Zm9vICdiYXInIGJheg' ]}"#);
assert_eq!(
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Str {
text: r#"foo 'bar' baz"#.into()
}
},
Token::Eof
]
);
Ok(())
}
#[test]
fn var_surrounded() -> Result<()> {
let mut p = Parser::new("Hello ${[ foo ]}!");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Raw {
text: "Hello ".to_string()
@@ -549,13 +611,15 @@ mod tests {
Token::Eof,
]
);
Ok(())
}
#[test]
fn fn_simple() {
fn fn_simple() -> Result<()> {
let mut p = Parser::new("${[ foo() ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -566,13 +630,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_dot_name() {
fn fn_dot_name() -> Result<()> {
let mut p = Parser::new("${[ foo.bar.baz() ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -583,13 +649,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_ident_arg() {
fn fn_ident_arg() -> Result<()> {
let mut p = Parser::new("${[ foo(a=bar) ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -603,13 +671,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_ident_args() {
fn fn_ident_args() -> Result<()> {
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -633,13 +703,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_mixed_args() {
fn fn_mixed_args() -> Result<()> {
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb='baz \'hi\'', c=qux, z=true ) ]}"#);
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -669,13 +741,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_nested() {
fn fn_nested() -> Result<()> {
let mut p = Parser::new("${[ foo(b=bar()) ]}");
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -692,13 +766,15 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn fn_nested_args() {
fn fn_nested_args() -> Result<()> {
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b='i'), c='o') ]}"#);
assert_eq!(
p.parse().tokens,
p.parse()?.tokens,
vec![
Token::Tag {
val: Val::Fn {
@@ -730,10 +806,12 @@ mod tests {
Token::Eof
]
);
Ok(())
}
#[test]
fn token_display_var() {
fn token_display_var() -> Result<()> {
assert_eq!(
Val::Var {
name: "foo".to_string()
@@ -741,21 +819,38 @@ mod tests {
.to_string(),
"foo"
);
Ok(())
}
#[test]
fn token_display_str() {
fn token_display_str() -> Result<()> {
assert_eq!(
Val::Str {
text: "Hello You".to_string()
}
.to_string(),
"'Hello You'"
);
Ok(())
}
#[test]
fn token_display_complex_str() -> Result<()> {
assert_eq!(
Val::Str {
text: "Hello 'You'".to_string()
}
.to_string(),
"'Hello \'You\''"
"b64'SGVsbG8gJ1lvdSc'"
);
Ok(())
}
#[test]
fn token_null_fn_arg() {
fn token_null_fn_arg() -> Result<()> {
assert_eq!(
Val::Fn {
name: "fn".to_string(),
@@ -775,10 +870,12 @@ mod tests {
.to_string(),
r#"fn(a='aaa')"#
);
Ok(())
}
#[test]
fn token_display_fn() {
fn token_display_fn() -> Result<()> {
assert_eq!(
Token::Tag {
val: Val::Fn {
@@ -787,7 +884,7 @@ mod tests {
FnArg {
name: "arg".to_string(),
value: Val::Str {
text: "v".to_string()
text: "v 'x'".to_string()
}
},
FnArg {
@@ -800,12 +897,14 @@ mod tests {
}
}
.to_string(),
r#"${[ foo(arg='v', arg2=my_var) ]}"#
r#"${[ foo(arg=b64'diAneCc', arg2=my_var) ]}"#
);
Ok(())
}
#[test]
fn tokens_display() {
fn tokens_display() -> Result<()> {
assert_eq!(
Tokens {
tokens: vec![
@@ -827,5 +926,7 @@ mod tests {
.to_string(),
r#"${[ my_var ]} Some cool text ${[ 'Hello World' ]}"#
);
Ok(())
}
}

View File

@@ -1,109 +1,117 @@
use crate::{FnArg, Parser, Token, Tokens, Val};
use crate::error::Error::{RenderStackExceededError, VariableNotFound};
use crate::error::Result;
use crate::{Parser, Token, Tokens, Val};
use log::warn;
use serde_json::json;
use std::collections::HashMap;
use std::future::Future;
const MAX_DEPTH: usize = 50;
pub trait TemplateCallback {
fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> impl Future<Output = Result<String, String>> + Send;
) -> impl Future<Output = Result<String>> + Send;
}
pub async fn render_json_value_raw<T: TemplateCallback>(
v: serde_json::Value,
vars: &HashMap<String, String>,
cb: &T,
) -> serde_json::Value {
match v {
serde_json::Value::String(s) => json!(parse_and_render(&s, vars, cb).await),
) -> Result<serde_json::Value> {
let v = match v {
serde_json::Value::String(s) => json!(parse_and_render(&s, vars, cb).await?),
serde_json::Value::Array(a) => {
let mut new_a = Vec::new();
for v in a {
new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await)
new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await?)
}
json!(new_a)
}
serde_json::Value::Object(o) => {
let mut new_o = serde_json::Map::new();
for (k, v) in o {
let key = Box::pin(parse_and_render(&k, vars, cb)).await;
let value = Box::pin(render_json_value_raw(v, vars, cb)).await;
let key = Box::pin(parse_and_render(&k, vars, cb)).await?;
let value = Box::pin(render_json_value_raw(v, vars, cb)).await?;
new_o.insert(key, value);
}
json!(new_o)
}
v => v,
}
};
Ok(v)
}
async fn parse_and_render_at_depth<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
depth: usize,
) -> Result<String> {
let mut p = Parser::new(template);
let tokens = p.parse()?;
render(tokens, vars, cb, depth + 1).await
}
pub async fn parse_and_render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb).await
) -> Result<String> {
parse_and_render_at_depth(template, vars, cb, 1).await
}
pub async fn render<T: TemplateCallback>(
tokens: Tokens,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
mut depth: usize,
) -> Result<String> {
depth += 1;
if depth > MAX_DEPTH {
return Err(RenderStackExceededError);
}
let mut doc_str: Vec<String> = Vec::new();
for t in tokens.tokens {
match t {
Token::Raw { text } => doc_str.push(text),
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb).await),
Token::Tag { val } => doc_str.push(render_value(val, &vars, cb, depth).await?),
Token::Eof => {}
}
}
doc_str.join("")
Ok(doc_str.join(""))
}
async fn render_tag<T: TemplateCallback>(
async fn render_value<T: TemplateCallback>(
val: Val,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
match val {
Val::Str { text } => text.into(),
depth: usize,
) -> Result<String> {
let v = match val {
Val::Str { text } => {
let r = Box::pin(parse_and_render_at_depth(&text, vars, cb, depth)).await?;
r.to_string()
}
Val::Var { name } => match vars.get(name.as_str()) {
Some(v) => {
let r = Box::pin(parse_and_render(v, vars, cb)).await;
let r = Box::pin(parse_and_render_at_depth(v, vars, cb, depth)).await?;
r.to_string()
}
None => "".into(),
None => return Err(VariableNotFound(name)),
},
Val::Bool { value } => value.to_string(),
Val::Fn { name, args } => {
let empty = "".to_string();
// let empty = "".to_string();
let mut resolved_args: HashMap<String, String> = HashMap::new();
for a in args {
let (k, v) = match a {
FnArg {
name,
value: Val::Str { text },
} => (name.to_string(), text.to_string()),
FnArg {
name,
value: Val::Var { name: var_name },
} => (
name.to_string(),
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
),
FnArg { name, value: val } => {
let r = Box::pin(render_tag(val.clone(), vars, cb)).await;
(name.to_string(), r)
}
};
resolved_args.insert(k, v);
let v = Box::pin(render_value(a.value, vars, cb, depth)).await?;
resolved_args.insert(a.name, v);
}
match cb.run(name.as_str(), resolved_args.clone()).await {
Ok(s) => s,
@@ -114,11 +122,15 @@ async fn render_tag<T: TemplateCallback>(
}
}
Val::Null => "".into(),
}
};
Ok(v)
}
#[cfg(test)]
mod parse_and_render_tests {
use crate::error::Error::{RenderError, RenderStackExceededError, VariableNotFound};
use crate::error::Result;
use crate::renderer::TemplateCallback;
use crate::*;
use std::collections::HashMap;
@@ -126,44 +138,43 @@ mod parse_and_render_tests {
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
todo!()
}
}
#[tokio::test]
async fn render_empty() {
async fn render_empty() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_text_only() {
async fn render_text_only() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_simple() {
async fn render_simple() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_recursive_var() {
async fn render_recursive_var() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let mut vars = HashMap::new();
@@ -172,49 +183,71 @@ mod parse_and_render_tests {
vars.insert("baz".to_string(), "baz".to_string());
let result = "foo: bar: baz";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_surrounded() {
async fn render_missing_var() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let vars = HashMap::new();
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
Err(VariableNotFound("foo".to_string()))
);
Ok(())
}
#[tokio::test]
async fn render_self_referencing_var() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let mut vars = HashMap::new();
vars.insert("foo".to_string(), "${[ foo ]}".to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
Err(RenderStackExceededError)
);
Ok(())
}
#[tokio::test]
async fn render_surrounded() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_valid_fn() {
async fn render_valid_fn() -> Result<()> {
let vars = HashMap::new();
let template = r#"${[ say_hello(a='John', b='Kate') ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
Ok(format!("{fn_name}: {}, {:?} {:?}", args.len(), args.get("a"), args.get("b")))
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result);
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result);
Ok(())
}
#[tokio::test]
async fn render_nested_fn() {
async fn render_fn_arg() -> Result<()> {
let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#;
let template = r#"${[ upper(foo='bar') ]}"#;
let result = r#"BAR"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
@@ -223,80 +256,142 @@ mod parse_and_render_tests {
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_fn_err() {
let vars = HashMap::new();
let template = r#"${[ error() ]}"#;
let result = r#""#;
async fn render_fn_b64_arg_template() -> Result<()> {
let mut vars = HashMap::new();
vars.insert("foo".to_string(), "bar".to_string());
let template = r#"${[ upper(foo=b64'Zm9vICdiYXInIGJheg') ]}"#;
let result = r#"FOO 'BAR' BAZ"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
Err("Failed to do it!".to_string())
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
Ok(match fn_name {
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result.to_string());
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_fn_arg_template() -> Result<()> {
let mut vars = HashMap::new();
vars.insert("foo".to_string(), "bar".to_string());
let template = r#"${[ upper(foo='${[ foo ]}') ]}"#;
let result = r#"BAR"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_nested_fn() -> Result<()> {
let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
}
#[tokio::test]
async fn render_fn_err() -> Result<()> {
let vars = HashMap::new();
let template = r#"${[ error() ]}"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
Err(RenderError("Failed to do it!".to_string()))
}
}
assert_eq!(
parse_and_render(template, &vars, &CB {}).await,
Err(RenderError("Failed to do it!".to_string()))
);
Ok(())
}
}
#[cfg(test)]
mod render_json_value_raw_tests {
use crate::error::Result;
use crate::{render_json_value_raw, TemplateCallback};
use serde_json::json;
use std::collections::HashMap;
use crate::{render_json_value_raw, TemplateCallback};
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
todo!()
}
}
#[tokio::test]
async fn render_json_value_string() {
async fn render_json_value_string() -> Result<()> {
let v = json!("${[a]}");
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!("aaa"))
assert_eq!(render_json_value_raw(v, &vars, &EmptyCB {}).await?, json!("aaa"));
Ok(())
}
#[tokio::test]
async fn render_json_value_array() {
async fn render_json_value_array() -> Result<()> {
let v = json!(["${[a]}", "${[a]}"]);
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!(["aaa", "aaa"]))
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!(result, json!(["aaa", "aaa"]));
Ok(())
}
#[tokio::test]
async fn render_json_value_object() {
async fn render_json_value_object() -> Result<()> {
let v = json!({"${[a]}": "${[a]}"});
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!({"aaa": "aaa"}))
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!(result, json!({"aaa": "aaa"}));
Ok(())
}
#[tokio::test]
async fn render_json_value_nested() {
async fn render_json_value_nested() -> Result<()> {
let v = json!([
123,
{"${[a]}": "${[a]}"},
@@ -308,7 +403,7 @@ mod render_json_value_raw_tests {
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await;
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!(
result,
json!([
@@ -319,6 +414,8 @@ mod render_json_value_raw_tests {
false,
{"x": ["aaa"]}
])
)
);
Ok(())
}
}

View File

@@ -2,7 +2,6 @@ use crate::error::Error::GenericError;
use crate::error::Result;
use crate::manager::WebsocketManager;
use crate::render::render_request;
use chrono::Utc;
use log::{info, warn};
use std::str::FromStr;
use tauri::http::{HeaderMap, HeaderName};
@@ -116,7 +115,7 @@ pub(crate) async fn send<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await?;
let mut ws_manager = ws_manager.lock().await;
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
@@ -147,42 +146,21 @@ pub(crate) async fn close<R: Runtime>(
ws_manager: State<'_, Mutex<WebsocketManager>>,
) -> Result<WebsocketConnection> {
let connection = get_websocket_connection(&window, connection_id).await?;
let request = get_websocket_request(&window, &connection.request_id)
.await?
.ok_or(GenericError("WebSocket Request not found".to_string()))?;
let mut ws_manager = ws_manager.lock().await;
if let Err(e) = ws_manager.send(&connection.id, Message::Close(None)).await {
warn!("Failed to close WebSocket connection: {e:?}");
};
upsert_websocket_event(
let connection = upsert_websocket_connection(
&window,
WebsocketEvent {
connection_id: connection.id.clone(),
request_id: request.id.clone(),
workspace_id: request.workspace_id.clone(),
is_server: false,
message_type: WebsocketEventType::Close,
..Default::default()
&WebsocketConnection {
state: WebsocketConnectionState::Closing,
..connection
},
&UpdateSource::Window,
)
.await
.unwrap();
let connection = upsert_websocket_connection(
&window,
&WebsocketConnection {
state: WebsocketConnectionState::Closed,
elapsed: Utc::now()
.naive_utc()
.signed_duration_since(connection.created_at)
.num_milliseconds() as i32,
..connection.clone()
},
&UpdateSource::Window,
)
.await?;
let mut ws_manager = ws_manager.lock().await;
if let Err(e) = ws_manager.close(&connection.id).await {
warn!("Failed to close WebSocket connection: {e:?}");
};
Ok(connection)
}
@@ -214,7 +192,7 @@ pub(crate) async fn connect<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await?;
let mut headers = HeaderMap::new();
if let Some(auth_name) = request.authentication_type.clone() {
@@ -264,42 +242,6 @@ pub(crate) async fn connect<R: Runtime>(
let (receive_tx, mut receive_rx) = mpsc::channel::<Message>(128);
let mut ws_manager = ws_manager.lock().await;
{
let connection_id = connection.id.clone();
let request_id = request.id.to_string();
let workspace_id = request.workspace_id.clone();
let window = window.clone();
tokio::spawn(async move {
while let Some(message) = receive_rx.recv().await {
upsert_websocket_event(
&window,
WebsocketEvent {
connection_id: connection_id.clone(),
request_id: request_id.clone(),
workspace_id: workspace_id.clone(),
is_server: true,
message_type: match message {
Message::Text(_) => WebsocketEventType::Text,
Message::Binary(_) => WebsocketEventType::Binary,
Message::Ping(_) => WebsocketEventType::Ping,
Message::Pong(_) => WebsocketEventType::Pong,
Message::Close(_) => WebsocketEventType::Close,
// Raw frame will never happen during a read
Message::Frame(_) => WebsocketEventType::Frame,
},
message: message.into_data().into(),
..Default::default()
},
&UpdateSource::Window,
)
.await
.unwrap();
}
info!("Websocket connection closed");
});
}
let (url, url_parameters) = apply_path_placeholders(&request.url, request.url_parameters);
// Add URL parameters to URL
@@ -331,6 +273,21 @@ pub(crate) async fn connect<R: Runtime>(
}
};
upsert_websocket_event(
&window,
WebsocketEvent {
connection_id: connection.id.clone(),
request_id: request.id.clone(),
workspace_id: connection.workspace_id.clone(),
is_server: false,
message_type: WebsocketEventType::Open,
..Default::default()
},
&UpdateSource::Window,
)
.await
.unwrap();
let response_headers = response
.headers()
.into_iter()
@@ -353,5 +310,74 @@ pub(crate) async fn connect<R: Runtime>(
)
.await?;
{
let connection_id = connection.id.clone();
let request_id = request.id.to_string();
let workspace_id = request.workspace_id.clone();
let window = window.clone();
let connection = connection.clone();
let mut has_written_close = false;
tokio::spawn(async move {
while let Some(message) = receive_rx.recv().await {
if let Message::Close(_) = message {
has_written_close = true;
}
upsert_websocket_event(
&window,
WebsocketEvent {
connection_id: connection_id.clone(),
request_id: request_id.clone(),
workspace_id: workspace_id.clone(),
is_server: true,
message_type: match message {
Message::Text(_) => WebsocketEventType::Text,
Message::Binary(_) => WebsocketEventType::Binary,
Message::Ping(_) => WebsocketEventType::Ping,
Message::Pong(_) => WebsocketEventType::Pong,
Message::Close(_) => WebsocketEventType::Close,
// Raw frame will never happen during a read
Message::Frame(_) => WebsocketEventType::Frame,
},
message: message.into_data().into(),
..Default::default()
},
&UpdateSource::Window,
)
.await
.unwrap();
}
info!("Websocket connection closed");
if !has_written_close {
upsert_websocket_event(
&window,
WebsocketEvent {
connection_id: connection_id.clone(),
request_id: request_id.clone(),
workspace_id: workspace_id.clone(),
is_server: true,
message_type: WebsocketEventType::Close,
..Default::default()
},
&UpdateSource::Window,
)
.await
.unwrap();
}
upsert_websocket_connection(
&window,
&WebsocketConnection {
workspace_id: request.workspace_id.clone(),
request_id: request_id.to_string(),
state: WebsocketConnectionState::Closed,
..connection
},
&UpdateSource::Window,
)
.await
.unwrap();
});
}
Ok(connection)
}

View File

@@ -41,40 +41,4 @@ pub(crate) async fn ws_connect(
)
.await?;
Ok((stream, response))
}
#[cfg(test)]
mod tests {
use crate::connect::ws_connect;
use crate::error::Result;
use futures_util::{SinkExt, StreamExt};
use std::time::Duration;
use tokio::time::timeout;
use tokio_tungstenite::tungstenite::Message;
#[tokio::test]
async fn test_connection() -> Result<()> {
let (stream, response) = ws_connect("wss://echo.websocket.org/", Default::default()).await?;
assert_eq!(response.status(), 101);
let (mut write, mut read) = stream.split();
let task = tokio::spawn(async move {
while let Some(Ok(message)) = read.next().await {
if message.is_text() && message.to_text().unwrap() == "Hello" {
return message;
}
}
panic!("Didn't receive text message");
});
write.send(Message::Text("Hello".into())).await?;
let task = timeout(Duration::from_secs(3), task);
let message = task.await.unwrap().unwrap();
assert_eq!(message.into_text().unwrap(), "Hello");
Ok(())
}
}
}

View File

@@ -13,6 +13,9 @@ pub enum Error {
#[error("Plugin error: {0}")]
PluginError(#[from] yaak_plugins::error::Error),
#[error("Render error: {0}")]
TemplateError(#[from] yaak_templates::error::Error),
#[error("WebSocket error: {0}")]
GenericError(String),
}

View File

@@ -1,11 +1,11 @@
mod commands;
mod connect;
mod error;
pub mod error;
mod manager;
mod render;
use crate::commands::{
connect, close, delete_connection, delete_connections, delete_request, duplicate_request,
close, connect, delete_connection, delete_connections, delete_request, duplicate_request,
list_connections, list_events, list_requests, send, upsert_request,
};
use crate::manager::WebsocketManager;
@@ -31,7 +31,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.setup(|app, _api| {
let manager = WebsocketManager::new();
app.manage(Mutex::new(manager));
Ok(())
})
.build()

View File

@@ -2,7 +2,7 @@ use crate::connect::ws_connect;
use crate::error::Result;
use futures_util::stream::SplitSink;
use futures_util::{SinkExt, StreamExt};
use log::debug;
use log::{debug, warn};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::net::TcpStream;
@@ -32,19 +32,27 @@ impl WebsocketManager {
headers: HeaderMap<HeaderValue>,
receive_tx: mpsc::Sender<Message>,
) -> Result<Response> {
let connections = self.connections.clone();
let connection_id = id.to_string();
let tx = receive_tx.clone();
let (stream, response) = ws_connect(url, headers).await?;
let (write, mut read) = stream.split();
self.connections.lock().await.insert(id.to_string(), write);
let tx = receive_tx.clone();
connections.lock().await.insert(id.to_string(), write);
tauri::async_runtime::spawn(async move {
while let Some(Ok(message)) = read.next().await {
debug!("Received websocket message {message:?}");
if message.is_close() {
return;
while let Some(msg) = read.next().await {
match msg {
Err(e) => {
warn!("Broken websocket connection: {}", e);
break;
}
Ok(message) => tx.send(message).await.unwrap(),
}
tx.send(message).await.unwrap();
}
debug!("Connection {} closed", connection_id);
connections.lock().await.remove(&connection_id);
});
Ok(response)
}
@@ -59,4 +67,15 @@ impl WebsocketManager {
connection.send(msg).await?;
Ok(())
}
pub async fn close(&mut self, id: &str) -> Result<()> {
debug!("Closing websocket");
let mut connections = self.connections.lock().await;
let connection = match connections.get_mut(id) {
None => return Ok(()),
Some(c) => c,
};
connection.close().await?;
Ok(())
}
}

View File

@@ -1,3 +1,4 @@
use crate::error::Result;
use std::collections::BTreeMap;
use yaak_models::models::{Environment, HttpRequestHeader, WebsocketRequest};
use yaak_models::render::make_vars_hashmap;
@@ -8,33 +9,33 @@ pub async fn render_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> WebsocketRequest {
) -> Result<WebsocketRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut headers = Vec::new();
for p in r.headers.clone() {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: parse_and_render(&p.name, vars, cb).await,
value: parse_and_render(&p.value, vars, cb).await,
name: parse_and_render(&p.name, vars, cb).await?,
value: parse_and_render(&p.value, vars, cb).await?,
id: p.id,
})
}
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await);
authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
}
let url = parse_and_render(r.url.as_str(), vars, cb).await;
let url = parse_and_render(r.url.as_str(), vars, cb).await?;
let message = parse_and_render(&r.message.clone(), vars, cb).await;
let message = parse_and_render(&r.message.clone(), vars, cb).await?;
WebsocketRequest {
Ok(WebsocketRequest {
url,
headers,
authentication,
message,
..r.to_owned()
}
})
}

View File

@@ -5,7 +5,6 @@ import { InlineCode } from '../components/core/InlineCode';
import { VStack } from '../components/core/Stacks';
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
import { pluralizeCount } from '../lib/pluralize';
@@ -42,7 +41,6 @@ export const createFolder = createFastMutation<
patch.sortPriority = patch.sortPriority || -Date.now();
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
},
onSettled: () => trackEvent('folder', 'create'),
});
export const syncWorkspace = createFastMutation<

View File

@@ -1,14 +1,10 @@
import type { WebsocketConnection } from '@yaakapp-internal/models';
import { deleteWebsocketConnection as cmdDeleteWebsocketConnection } from '@yaakapp-internal/ws';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
export const deleteWebsocketConnection = createFastMutation({
mutationKey: ['delete_websocket_connection'],
mutationFn: async function (connection: WebsocketConnection) {
return cmdDeleteWebsocketConnection(connection.id);
},
onSuccess: async () => {
trackEvent('websocket_connection', 'delete');
},
});

View File

@@ -1,14 +1,10 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from '@yaakapp-internal/ws';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
export const deleteWebsocketConnections = createFastMutation({
mutationKey: ['delete_websocket_connections'],
mutationFn: async function (request: WebsocketRequest) {
return cmdDeleteWebsocketConnections(request.id);
},
onSuccess: async () => {
trackEvent('websocket_connection', 'delete_many');
},
});

View File

@@ -2,7 +2,6 @@ import type { WebsocketRequest } from '@yaakapp-internal/models';
import { deleteWebsocketRequest as cmdDeleteWebsocketRequest } from '@yaakapp-internal/ws';
import { InlineCode } from '../components/core/InlineCode';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { showConfirmDelete } from '../lib/confirm';
import { resolvedModelName } from '../lib/resolvedModelName';
@@ -24,7 +23,4 @@ export const deleteWebsocketRequest = createFastMutation({
return cmdDeleteWebsocketRequest(request.id);
},
onSuccess: async () => {
trackEvent('websocket_request', 'delete');
},
});

View File

@@ -1,16 +1,13 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { duplicateWebsocketRequest as cmdDuplicateWebsocketRequest } from '@yaakapp-internal/ws';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
export const duplicateWebsocketRequest = createFastMutation({
mutationKey: ['delete_websocket_connection'],
mutationFn: async function (request: WebsocketRequest) {
return cmdDuplicateWebsocketRequest(request.id);
mutationFn: async function (requestId: string) {
return cmdDuplicateWebsocketRequest(requestId);
},
onSuccess: async (request) => {
trackEvent('websocket_request', 'duplicate');
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },

View File

@@ -1,7 +1,6 @@
import { SettingsTab } from '../components/Settings/SettingsTab';
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
@@ -11,7 +10,6 @@ export const openSettings = createFastMutation<void, string, SettingsTab | null>
const workspaceId = getActiveWorkspaceId();
if (workspaceId == null) return;
trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` });
const location = router.buildLocation({
to: '/workspaces/$workspaceId/settings',
params: { workspaceId },

View File

@@ -1,20 +1,11 @@
import { open } from '@tauri-apps/plugin-dialog';
import { applySync, calculateSyncFsOnly } from '@yaakapp-internal/sync';
import { createFastMutation } from '../hooks/useFastMutation';
import { showSimpleAlert } from '../lib/alert';
import { router } from '../lib/router';
export const openWorkspaceFromSyncDir = createFastMutation<void>({
export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
mutationKey: [],
mutationFn: async () => {
const dir = await open({
title: 'Select Workspace Directory',
directory: true,
multiple: false,
});
if (dir == null) return;
mutationFn: async (dir) => {
const ops = await calculateSyncFsOnly(dir);
const workspace = ops

View File

@@ -2,7 +2,6 @@ import type { WebsocketRequest } from '@yaakapp-internal/models';
import { upsertWebsocketRequest as cmdUpsertWebsocketRequest } from '@yaakapp-internal/ws';
import { differenceInMilliseconds } from 'date-fns';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
export const upsertWebsocketRequest = createFastMutation<
@@ -16,12 +15,11 @@ export const upsertWebsocketRequest = createFastMutation<
const isNew = differenceInMilliseconds(new Date(), request.createdAt + 'Z') < 100;
if (isNew) {
trackEvent('websocket_request', 'create');
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
} else trackEvent('websocket_request', 'update');
}
},
});

View File

@@ -1,7 +1,5 @@
import type { Workspace } from '@yaakapp-internal/models';
import { differenceInMilliseconds } from 'date-fns';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
export const upsertWorkspace = createFastMutation<
@@ -11,10 +9,4 @@ export const upsertWorkspace = createFastMutation<
>({
mutationKey: ['upsert_workspace'],
mutationFn: (workspace) => invokeCmd<Workspace>('cmd_update_workspace', { workspace }),
onSuccess: async (workspace) => {
const isNew = differenceInMilliseconds(new Date(), workspace.createdAt + 'Z') < 100;
if (isNew) trackEvent('workspace', 'create');
else trackEvent('workspace', 'update');
},
});

View File

@@ -1,80 +0,0 @@
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props<T> {
request: T;
}
export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
return (
<VStack className="py-2 overflow-y-auto h-full" space={2}>
<Input
useTemplating
autocompleteVariables
stateKey={`basic.username.${request.id}`}
forceUpdateKey={request.id}
placeholder="username"
label="Username"
name="username"
size="sm"
defaultValue={`${request.authentication.username}`}
onChange={(username: string) => {
if (request.model === 'http_request') {
updateHttpRequest.mutate({
id: request.id,
update: (r: HttpRequest) => ({
...r,
authentication: { password: r.authentication.password, username },
}),
});
} else {
updateGrpcRequest.mutate({
id: request.id,
update: (r: GrpcRequest) => ({
...r,
authentication: { password: r.authentication.password, username },
}),
});
}
}}
/>
<Input
useTemplating
autocompleteVariables
forceUpdateKey={request?.id}
stateKey={`basic.password.${request.id}`}
placeholder="password"
label="Password"
name="password"
size="sm"
type="password"
defaultValue={`${request.authentication.password}`}
onChange={(password: string) => {
if (request.model === 'http_request') {
updateHttpRequest.mutate({
id: request.id,
update: (r: HttpRequest) => ({
...r,
authentication: { username: r.authentication.username, password },
}),
});
} else {
updateGrpcRequest.mutate({
id: request.id,
update: (r: GrpcRequest) => ({
...r,
authentication: { username: r.authentication.username, password },
}),
});
}
}}
/>
</VStack>
);
}

View File

@@ -1,49 +0,0 @@
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props<T> {
request: T;
}
export function BearerAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
return (
<VStack className="my-2" space={2}>
<Input
useTemplating
autocompleteVariables
placeholder="token"
stateKey={`bearer.${request.id}`}
type="password"
label="Token"
name="token"
size="sm"
defaultValue={`${request.authentication.token}`}
onChange={(token: string) => {
if (request.model === 'http_request') {
updateHttpRequest.mutate({
id: request.id ?? null,
update: (r: HttpRequest) => ({
...r,
authentication: { token },
}),
});
} else {
updateGrpcRequest.mutate({
id: request.id ?? null,
update: (r: GrpcRequest) => ({
...r,
authentication: { token },
}),
});
}
}}
/>
</VStack>
);
}

View File

@@ -3,7 +3,7 @@ import { useMemo, type ReactNode } from 'react';
import { useSaveResponse } from '../hooks/useSaveResponse';
import { useToggle } from '../hooks/useToggle';
import { isProbablyTextContentType } from '../lib/contentType';
import { getContentTypeHeader } from '../lib/model_util';
import { getContentTypeFromHeaders } from '../lib/model_util';
import { getResponseBodyText } from '../lib/responseBody';
import { CopyButton } from './CopyButton';
import { Banner } from './core/Banner';
@@ -24,7 +24,7 @@ export function ConfirmLargeResponse({ children, response }: Props) {
const { mutate: saveResponse } = useSaveResponse(response);
const [showLargeResponse, toggleShowLargeResponse] = useToggle();
const isProbablyText = useMemo(() => {
const contentType = getContentTypeHeader(response.headers);
const contentType = getContentTypeFromHeaders(response.headers);
return isProbablyTextContentType(contentType);
}, [response.headers]);

View File

@@ -20,7 +20,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
const [syncConfig, setSyncConfig] = useState<{
filePath: string | null;
initGit?: boolean;
}>({ filePath: null, initGit: true });
}>({ filePath: null, initGit: false });
return (
<VStack
@@ -62,8 +62,8 @@ export function CreateWorkspaceDialog({ hide }: Props) {
<SyncToFilesystemSetting
onChange={setSyncConfig}
onCreateNewWorkspace={hide}
value={syncConfig}
allowNonEmptyDirectory // Will do initial import when the workspace is created
/>
<Button type="submit" color="primary" className="ml-auto mt-3">
Create Workspace

View File

@@ -34,7 +34,7 @@ interface Props<T> {
inputs: FormInput[] | undefined | null;
onChange: (value: T) => void;
data: T;
useTemplating?: boolean;
autocompleteFunctions?: boolean;
autocompleteVariables?: boolean;
stateKey: string;
disabled?: boolean;
@@ -44,8 +44,8 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
inputs,
data,
onChange,
useTemplating,
autocompleteVariables,
autocompleteFunctions,
stateKey,
disabled,
}: Props<T>) {
@@ -62,7 +62,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
inputs={inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
useTemplating={useTemplating}
autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables}
data={data}
/>
@@ -71,13 +71,16 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
function FormInputs<T extends Record<string, JsonPrimitive>>({
inputs,
autocompleteFunctions,
autocompleteVariables,
stateKey,
useTemplating,
setDataAttr,
data,
disabled,
}: Pick<Props<T>, 'inputs' | 'useTemplating' | 'autocompleteVariables' | 'stateKey' | 'data'> & {
}: Pick<
Props<T>,
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
> & {
setDataAttr: (name: string, value: JsonPrimitive) => void;
disabled?: boolean;
}) {
@@ -112,7 +115,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
key={i}
stateKey={stateKey}
arg={input}
useTemplating={useTemplating || false}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)}
value={
@@ -126,7 +129,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
key={i}
stateKey={stateKey}
arg={input}
useTemplating={useTemplating || false}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)}
value={
@@ -175,6 +178,8 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables}
/>
</div>
</details>
@@ -193,6 +198,8 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables}
/>
</Banner>
);
@@ -208,14 +215,14 @@ function TextArg({
arg,
onChange,
value,
useTemplating,
autocompleteFunctions,
autocompleteVariables,
stateKey,
}: {
arg: FormInputText;
value: string;
onChange: (v: string) => void;
useTemplating: boolean;
autocompleteFunctions: boolean;
autocompleteVariables: boolean;
stateKey: string;
}) {
@@ -233,7 +240,7 @@ function TextArg({
hideLabel={arg.label == null}
placeholder={arg.placeholder ?? undefined}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
useTemplating={useTemplating}
autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables}
stateKey={stateKey}
forceUpdateKey={stateKey}
@@ -245,14 +252,14 @@ function EditorArg({
arg,
onChange,
value,
useTemplating,
autocompleteFunctions,
autocompleteVariables,
stateKey,
}: {
arg: FormInputEditor;
value: string;
onChange: (v: string) => void;
useTemplating: boolean;
autocompleteFunctions: boolean;
autocompleteVariables: boolean;
stateKey: string;
}) {
@@ -286,7 +293,7 @@ function EditorArg({
heightMode="auto"
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
placeholder={arg.placeholder ?? undefined}
useTemplating={useTemplating}
autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables}
stateKey={stateKey}
forceUpdateKey={forceUpdateKey}

View File

@@ -32,10 +32,13 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const { baseEnvironment, subEnvironments, allEnvironments } = useEnvironments();
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironment?.id ?? baseEnvironment?.id ?? null,
initialEnvironment?.id ?? null,
);
const selectedEnvironment = allEnvironments.find((e) => e.id === selectedEnvironmentId);
const selectedEnvironment =
selectedEnvironmentId != null
? allEnvironments.find((e) => e.id === selectedEnvironmentId)
: baseEnvironment;
const handleCreateEnvironment = async () => {
if (baseEnvironment == null) return;
@@ -55,7 +58,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
<div className="min-w-0 h-full overflow-y-auto pt-1">
<SidebarButton
active={selectedEnvironment?.id == baseEnvironment?.id}
onClick={() => setSelectedEnvironmentId(baseEnvironment?.id ?? null)}
onClick={() => setSelectedEnvironmentId(null)}
environment={null}
rightSlot={
<IconButton
@@ -82,6 +85,11 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
active={selectedEnvironment?.id === e.id}
environment={e}
onClick={() => setSelectedEnvironmentId(e.id)}
onDelete={() => {
if (e.id === selectedEnvironmentId) {
setSelectedEnvironmentId(null);
}
}}
>
{e.name}
</SidebarButton>
@@ -90,11 +98,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
</aside>
)}
secondSlot={() =>
selectedEnvironmentId == null ? (
<div className="p-3 mt-10">
<Banner color="danger">No selected environment</Banner>
</div>
) : selectedEnvironment == null ? (
selectedEnvironment == null ? (
<div className="p-3 mt-10">
<Banner color="danger">
Failed to find selected environment <InlineCode>{selectedEnvironmentId}</InlineCode>
@@ -112,7 +116,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
};
const EnvironmentEditor = function ({
environment,
environment: activeEnvironment,
className,
}: {
environment: Environment;
@@ -123,8 +127,8 @@ const EnvironmentEditor = function ({
key: 'environmentValueVisibility',
fallback: true,
});
const { subEnvironments } = useEnvironments();
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
const { allEnvironments } = useEnvironments();
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
const handleChange = useCallback<PairEditorProps['onChange']>(
(variables) => updateEnvironment.mutate({ variables }),
[updateEnvironment],
@@ -132,38 +136,40 @@ const EnvironmentEditor = function ({
// Gather a list of env names from other environments, to help the user get them aligned
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
const allVariableNames =
environment == null
? [] // Nothing to autocomplete if we're in the base environment
: subEnvironments
.filter((e) => e.environmentId != null)
.flatMap((e) => e.variables.map((v) => v.name));
const options: GenericCompletionOption[] = [];
const isBaseEnv = activeEnvironment.environmentId == null;
if (isBaseEnv) {
return { options };
}
// Filter out empty strings and variables that already exist
const variableNames = allVariableNames.filter(
(name) => name != '' && !environment.variables.find((v) => v.name === name),
);
const uniqueVariableNames = [...new Set(variableNames)];
const options = uniqueVariableNames.map(
(name): GenericCompletionOption => ({
const allVariables = allEnvironments.flatMap((e) => e?.variables);
const allVariableNames = new Set(allVariables.map((v) => v?.name));
for (const name of allVariableNames) {
const containingEnvs = allEnvironments.filter((e) =>
e.variables.some((v) => v.name === name),
);
const isAlreadyInActive = containingEnvs.find((e) => e.id === activeEnvironment.id);
if (isAlreadyInActive) continue;
options.push({
label: name,
type: 'constant',
}),
);
detail: containingEnvs.map((e) => e.name).join(', '),
});
}
return { options };
}, [subEnvironments, environment]);
}, [activeEnvironment.environmentId, activeEnvironment.id, allEnvironments]);
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet, and is unusable
if (name === '') return true;
return name.match(/^[a-z][a-z0-9_-]*$/i) != null;
return name.match(/^[a-z_][a-z0-9_-]*$/i) != null;
}, []);
return (
<VStack space={4} className={classNames(className, 'pl-4')}>
<HStack space={2} className="justify-between">
<Heading className="w-full flex items-center gap-1">
<div>{environment?.name}</div>
<div>{activeEnvironment?.name}</div>
<IconButton
size="sm"
icon={valueVisibility.value ? 'eye' : 'eye_closed'}
@@ -176,17 +182,18 @@ const EnvironmentEditor = function ({
</HStack>
<div className="h-full pr-2 pb-2">
<PairOrBulkEditor
allowMultilineValues
preferenceName="environment"
nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={true}
forceUpdateKey={environment.id}
pairs={environment.variables}
valueAutocompleteVariables
valueAutocompleteFunctions
forceUpdateKey={activeEnvironment.id}
pairs={activeEnvironment.variables}
onChange={handleChange}
stateKey={`environment.${environment.id}`}
stateKey={`environment.${activeEnvironment.id}`}
/>
</div>
</VStack>
@@ -198,6 +205,7 @@ function SidebarButton({
className,
active,
onClick,
onDelete,
rightSlot,
environment,
}: {
@@ -205,6 +213,7 @@ function SidebarButton({
children: ReactNode;
active: boolean;
onClick: () => void;
onDelete?: () => void;
rightSlot?: ReactNode;
environment: Environment | null;
}) {
@@ -275,7 +284,11 @@ function SidebarButton({
color: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />,
onSelect: () => deleteEnvironment.mutate(),
onSelect: () => {
deleteEnvironment.mutate(undefined, {
onSuccess: onDelete,
});
},
},
]}
/>

View File

@@ -40,9 +40,12 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
return (
<PairEditor
valueAutocompleteFunctions
valueAutocompleteVariables
nameAutocompleteVariables
nameAutocompleteFunctions
allowFileValues
allowMultilineValues
pairs={pairs}
onChange={handleChange}
forceUpdateKey={forceUpdateKey}

View File

@@ -29,8 +29,11 @@ export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Prop
return (
<PairOrBulkEditor
allowMultilineValues
preferenceName="form_urlencoded"
valueAutocompleteFunctions
valueAutocompleteVariables
nameAutocompleteFunctions
nameAutocompleteVariables
namePlaceholder="entry_name"
valuePlaceholder="Value"

View File

@@ -39,7 +39,7 @@ interface TreeNode {
}
export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const [{ status }, { commit, add, unstage, push }] = useGit(syncDir);
const [{ status }, { commit, commitAndPush, add, unstage, push }] = useGit(syncDir);
const [message, setMessage] = useState<string>('');
const handleCreateCommit = async () => {
@@ -53,8 +53,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const handleCreateCommitAndPush = async () => {
try {
await commit.mutateAsync({ message });
await push.mutateAsync();
await commitAndPush.mutateAsync({ message });
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
onDone();
} catch (err) {
@@ -66,10 +65,13 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const allEntries = [];
const yaakEntries = [];
const externalEntries = [];
for (const entry of status.data?.entries ?? []) {
allEntries.push(entry);
if (entry.next == null && entry.prev == null) {
externalEntries.push(entry);
} else if (entry.next?.model === 'environment' || entry.prev?.model === 'environment') {
externalEntries.push(entry);
} else {
yaakEntries.push(entry);
}
@@ -184,7 +186,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
size="sm"
onClick={handleCreateCommit}
disabled={!hasAddedAnything}
isLoading={push.isPending || commit.isPending}
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
>
Commit
</Button>
@@ -193,7 +195,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
size="sm"
disabled={!hasAddedAnything}
onClick={handleCreateCommitAndPush}
isLoading={push.isPending || commit.isPending}
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
>
Commit and Push
</Button>

View File

@@ -336,12 +336,14 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
label: banner,
},
{
color: 'success',
label: 'Open Workspace Settings',
leftSlot: <Icon icon="settings" />,
onSelect() {
openWorkspaceSettings.mutate({ openSyncMenu: true });
},
},
{ type: 'separator' },
{
label: 'Hide This Message',
leftSlot: <Icon icon="eye_closed" />,
@@ -396,8 +398,8 @@ function SetupGitDropdown({
leftSlot: <Icon icon="magic_wand" />,
onSelect: initRepo,
},
{ type: 'separator' },
{
color: 'warning',
label: 'Hide This Message',
leftSlot: <Icon icon="eye_closed" />,
async onSelect() {

View File

@@ -183,7 +183,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
onChange={handleChangeVariables}
placeholder="{}"
stateKey={'graphql_vars.' + request.id}
useTemplating
autocompleteFunctions
autocompleteVariables
{...extraEditorProps}
/>

View File

@@ -67,7 +67,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
firstSlot={() =>
activeConnection && (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
<HStack className="pl-3 mb-1 font-mono text-sm">
<HStack className="pl-3 mb-1 font-mono text-sm text-text-subtle">
<HStack space={2}>
<span>{events.length} Messages</span>
{activeConnection.state !== 'closed' && (

View File

@@ -181,8 +181,8 @@ export function GrpcEditor({
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
language="json"
autocompleteFunctions
autocompleteVariables
useTemplating
forceUpdateKey={request.id}
defaultValue={request.message}
heightMode="auto"

View File

@@ -21,7 +21,9 @@ export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: P
<PairOrBulkEditor
preferenceName="headers"
stateKey={stateKey}
valueAutocompleteFunctions
valueAutocompleteVariables
nameAutocompleteFunctions
nameAutocompleteVariables
pairs={headers}
onChange={onChange}

View File

@@ -75,7 +75,7 @@ export function HttpAuthenticationEditor({ request }: Props) {
<DynamicForm
disabled={request.authentication.disabled}
autocompleteVariables
useTemplating
autocompleteFunctions
stateKey={`auth.${request.id}.${request.authenticationType}`}
inputs={authConfig.data.args}
data={request.authentication}

View File

@@ -7,12 +7,10 @@ import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useImportCurl } from '../hooks/useImportCurl';
import { useImportQuerystring } from '../hooks/useImportQuerystring';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
@@ -20,7 +18,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { resolvedModelName } from '../lib/resolvedModelName';
import { generateId } from '../lib/generateId';
import {
BODY_TYPE_BINARY,
@@ -30,8 +27,10 @@ import {
BODY_TYPE_JSON,
BODY_TYPE_NONE,
BODY_TYPE_OTHER,
BODY_TYPE_XML,
BODY_TYPE_XML, getContentTypeFromHeaders,
} from '../lib/model_util';
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
import { resolvedModelName } from '../lib/resolvedModelName';
import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge';
@@ -83,8 +82,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const [{ urlKey }] = useRequestEditor();
const contentType = useContentTypeFromHeaders(activeRequest.headers);
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
const contentType = getContentTypeFromHeaders(activeRequest.headers);
const authentication = useHttpAuthenticationSummaries();
const handleContentTypeChange = useCallback(
@@ -273,7 +272,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
const { updateKey } = useRequestUpdateKey(activeRequestId);
const { mutate: importCurl } = useImportCurl();
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
const handleBodyChange = useCallback(
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
@@ -314,17 +312,35 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
);
const handlePaste = useCallback(
(text: string) => {
(e: ClipboardEvent, text: string) => {
if (text.startsWith('curl ')) {
importCurl({ overwriteRequestId: activeRequestId, command: text });
} else {
// Only import query if pasted text contains entire querystring
importQuerystring(text);
const data = prepareImportQuerystring(text);
if (data != null) {
e.preventDefault(); // Prevent input onChange
updateRequest({ id: activeRequestId, update: data });
focusParamsTab();
// Wait for request to update, then refresh the UI
// TODO: Somehow make this deterministic
setTimeout(() => {
forceUrlRefresh();
forceParamsRefresh();
}, 100);
}
}
},
[activeRequestId, importCurl, importQuerystring],
[
activeRequestId,
focusParamsTab,
forceParamsRefresh,
forceUrlRefresh,
importCurl,
updateRequest,
],
);
const handleSend = useCallback(
() => sendRequest(activeRequest.id ?? null),
[activeRequest.id, sendRequest],
@@ -395,7 +411,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{activeRequest.bodyType === BODY_TYPE_JSON ? (
<Editor
forceUpdateKey={forceUpdateKey}
useTemplating
autocompleteFunctions
autocompleteVariables
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
@@ -407,7 +423,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
) : activeRequest.bodyType === BODY_TYPE_XML ? (
<Editor
forceUpdateKey={forceUpdateKey}
useTemplating
autocompleteFunctions
autocompleteVariables
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
@@ -446,7 +462,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
) : typeof activeRequest.bodyType === 'string' ? (
<Editor
forceUpdateKey={forceUpdateKey}
useTemplating
autocompleteFunctions
autocompleteVariables
language={languageFromContentType(contentType)}
placeholder="..."

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