mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 07:37:48 +01:00
Compare commits
54 Commits
v2025.1.0
...
v2025.2.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736025b12f | ||
|
|
cb9e9a67a3 | ||
|
|
93c323458f | ||
|
|
6f8c03d8c1 | ||
|
|
afd4228fcf | ||
|
|
d478e5a12e | ||
|
|
0db9ebe67d | ||
|
|
80ea5e6b91 | ||
|
|
cb773babe1 | ||
|
|
b9ed554aca | ||
|
|
f42f3d0e27 | ||
|
|
93ba5b6e5c | ||
|
|
be11d5968e | ||
|
|
0828599e4f | ||
|
|
f47d22c395 | ||
|
|
12233cb6f6 | ||
|
|
cdce2ac53a | ||
|
|
f4d0371060 | ||
|
|
787a0433cb | ||
|
|
60ea408e51 | ||
|
|
0db0cdfd6c | ||
|
|
26371e5f6b | ||
|
|
6b7c144a11 | ||
|
|
62f43ca24c | ||
|
|
fbf4d3c11e | ||
|
|
7a1a0689b0 | ||
|
|
9ead45d67a | ||
|
|
eb8153f409 | ||
|
|
80de232bec | ||
|
|
7af8c95fea | ||
|
|
2db72fe6ef | ||
|
|
d297e92a5a | ||
|
|
7e1da4395d | ||
|
|
7f8b0479e1 | ||
|
|
c8d6183456 | ||
|
|
9d5f7784c4 | ||
|
|
05ac836265 | ||
|
|
af7782c93b | ||
|
|
2b1431d041 | ||
|
|
9d8b7a5265 | ||
|
|
95c12ad291 | ||
|
|
dac2cec52f | ||
|
|
efe4eef1b7 | ||
|
|
a0e196a9e7 | ||
|
|
c6427dc724 | ||
|
|
8ce1e22b4e | ||
|
|
022d725e03 | ||
|
|
ed7fdb1b4c | ||
|
|
e510204b8c | ||
|
|
d31b4448df | ||
|
|
e420a0a45e | ||
|
|
84ecbe0cd6 | ||
|
|
6a63cc26b9 | ||
|
|
8ed0fd55c3 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal 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
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
37
README.md
37
README.md
@@ -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
199
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
/**
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
597
packages/plugin-runtime/src/PluginInstance.ts
Normal file
597
packages/plugin-runtime/src/PluginInstance.ts
Normal 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();
|
||||
// });
|
||||
// }
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
685
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -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";
|
||||
2
src-tauri/gen/schemas/acl-manifests.json
generated
2
src-tauri/gen/schemas/acl-manifests.json
generated
File diff suppressed because one or more lines are too long
10
src-tauri/gen/schemas/desktop-schema.json
generated
10
src-tauri/gen/schemas/desktop-schema.json
generated
@@ -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",
|
||||
|
||||
10
src-tauri/gen/schemas/macOS-schema.json
generated
10
src-tauri/gen/schemas/macOS-schema.json
generated
@@ -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",
|
||||
|
||||
@@ -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(¶ms);
|
||||
|
||||
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
43
src-tauri/src/error.rs
Normal 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
64
src-tauri/src/history.rs
Normal 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
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¬ification.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(())
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
25
src-tauri/src/uri_scheme.rs
Normal file
25
src-tauri/src/uri_scheme.rs
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
34
src-tauri/vendored/plugins/auth-jwt/build/index.js
generated
34
src-tauri/vendored/plugins/auth-jwt/build/index.js
generated
@@ -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) {
|
||||
|
||||
101
src-tauri/vendored/plugins/auth-oauth2/build/index.js
generated
101
src-tauri/vendored/plugins/auth-oauth2/build/index.js
generated
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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, };
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,7 @@ use tauri::{
|
||||
mod branch;
|
||||
mod callbacks;
|
||||
mod commands;
|
||||
mod error;
|
||||
pub mod error;
|
||||
mod fetch;
|
||||
mod git;
|
||||
mod merge;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const COMMANDS: &[&str] = &["activate", "check"];
|
||||
const COMMANDS: &[&str] = &["activate", "deactivate", "check"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS).build();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-check", "allow-activate"]
|
||||
permissions = ["allow-check", "allow-activate", "allow-deactivate"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
/**
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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" }
|
||||
|
||||
25
src-tauri/yaak-templates/src/error.rs
Normal file
25
src-tauri/yaak-templates/src/error.rs
Normal 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>;
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod format;
|
||||
pub mod parser;
|
||||
pub mod renderer;
|
||||
pub mod error;
|
||||
|
||||
pub use parser::*;
|
||||
pub use renderer::*;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -183,7 +183,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
onChange={handleChangeVariables}
|
||||
placeholder="{}"
|
||||
stateKey={'graphql_vars.' + request.id}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user