mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-06 05:19:05 -05:00
Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aadfbfdfca | ||
|
|
383fd05c6c | ||
|
|
be0a8fc27a | ||
|
|
648a1ac53c | ||
|
|
9fab37fb17 | ||
|
|
e0aaa33ccb | ||
|
|
20f7d20031 | ||
|
|
4d90bc78b1 | ||
|
|
97763a1301 | ||
|
|
d8b5a201b6 | ||
|
|
88e87a1999 | ||
|
|
2c4c1abd20 | ||
|
|
67026fc5b3 | ||
|
|
423a1a0a52 | ||
|
|
1abe01aa5a | ||
|
|
d0fde99b1c | ||
|
|
27901231dc | ||
|
|
1d9d80319b | ||
|
|
f62e90297d | ||
|
|
fcda6f8d32 | ||
|
|
021f2171d6 | ||
|
|
2562cf7c55 | ||
|
|
58873ea606 | ||
|
|
9d9e83d59f | ||
|
|
01d40f5b0d | ||
|
|
bdb1adcce1 | ||
|
|
9f6a3da8d3 | ||
|
|
158487e3a6 | ||
|
|
c1b18105b5 | ||
|
|
eb5ef7d7d5 | ||
|
|
6eb16afd96 | ||
|
|
9e68e276a1 | ||
|
|
af230a8f45 | ||
|
|
f9ac36caf0 | ||
|
|
a7a301ceba | ||
|
|
4166daf0a2 | ||
|
|
b52570bf58 | ||
|
|
1e27e1d8cb | ||
|
|
7047260697 | ||
|
|
fa33a89b63 | ||
|
|
00c0884616 | ||
|
|
a70768b61d | ||
|
|
8e3826b6c3 | ||
|
|
101efdd512 | ||
|
|
723e8d2874 | ||
|
|
385a369699 | ||
|
|
79362c81e5 | ||
|
|
bd1986f31f | ||
|
|
085b640b3c | ||
|
|
d07272003b | ||
|
|
bbf2b6dec0 | ||
|
|
399cd35b2b | ||
|
|
72dd768f55 | ||
|
|
053cbe49f9 | ||
|
|
862d85e48d | ||
|
|
a6d03cbeeb | ||
|
|
7d1ca1c232 | ||
|
|
261911b57e | ||
|
|
245054cd7d | ||
|
|
21b9e5a02b | ||
|
|
6d6012fe67 | ||
|
|
101582e540 | ||
|
|
0a932798a0 | ||
|
|
4609c95ad5 | ||
|
|
9d54e40aa8 | ||
|
|
9ec9222216 | ||
|
|
4d1dda0786 | ||
|
|
31605881ac | ||
|
|
4cd2e9cd31 | ||
|
|
13d959799a | ||
|
|
a6b18c23e1 | ||
|
|
041298b3f8 | ||
|
|
b400940f0e | ||
|
|
2e144f064d | ||
|
|
d8b1cadae6 | ||
|
|
c2f9760d08 | ||
|
|
a4c600cb48 | ||
|
|
bc3a5e3e58 | ||
|
|
d02883282f | ||
|
|
2c3fb25932 | ||
|
|
4c3a02ac53 | ||
|
|
1974d61aa4 | ||
|
|
0bcb092854 | ||
|
|
409620f533 | ||
|
|
4ae7f99264 | ||
|
|
3e9037f70a | ||
|
|
be82b67ed3 | ||
|
|
432b366105 | ||
|
|
42e70b941d | ||
|
|
3808215210 | ||
|
|
763a60982a | ||
|
|
a05679fd93 | ||
|
|
73c366dc27 | ||
|
|
c73f0b02bd | ||
|
|
0be7d0283b | ||
|
|
9615d3e29b | ||
|
|
749df338c5 | ||
|
|
3184c1b79e | ||
|
|
b52bf7cd56 | ||
|
|
d962d7f94b | ||
|
|
21e2a67c1e | ||
|
|
c188435524 | ||
|
|
8a7a7ba49d | ||
|
|
cbc40230bb | ||
|
|
bc4c3178c9 | ||
|
|
121fe5b3ea | ||
|
|
861609ddc0 | ||
|
|
e5070513ac | ||
|
|
f5c3798df9 | ||
|
|
469d12fede | ||
|
|
417a02744b | ||
|
|
81e78ef24c | ||
|
|
dad9cebb9e | ||
|
|
b3ede3d6d6 | ||
|
|
035fe54df0 | ||
|
|
5f8d99ba64 | ||
|
|
8c0f889dd2 | ||
|
|
84b8d130dc | ||
|
|
20b0b4fb69 | ||
|
|
8be9c4c388 | ||
|
|
a5333deb71 | ||
|
|
94d4227bc1 | ||
|
|
77cdea2f9f | ||
|
|
8b1ca4cb47 | ||
|
|
d3b8a42180 | ||
|
|
95f39c514a | ||
|
|
7cba082eb0 | ||
|
|
3b9b320be2 | ||
|
|
18664975a9 | ||
|
|
bb014b7c43 | ||
|
|
9fa0650647 | ||
|
|
b8c42677ca | ||
|
|
2eb3c2241c | ||
|
|
8fb7bbfe2e | ||
|
|
52eba74151 | ||
|
|
e651760713 | ||
|
|
82451a26f6 | ||
|
|
cc15f60fb6 | ||
|
|
2f8b2a81c7 | ||
|
|
6d4fdc91fe | ||
|
|
faca29c789 | ||
|
|
1ab937aae4 | ||
|
|
45fcea1954 | ||
|
|
73554078d1 | ||
|
|
a42a88de7b | ||
|
|
14a6079176 | ||
|
|
6c513616c0 | ||
|
|
cdf5f1b7a5 | ||
|
|
6566857d54 | ||
|
|
2e55a1bd6d | ||
|
|
e114a85c39 | ||
|
|
92be088e6c | ||
|
|
f1757ae427 | ||
|
|
ce885c3551 | ||
|
|
17657a4d04 | ||
|
|
b7f62b78b1 | ||
|
|
006284b99c | ||
|
|
bac3968aac | ||
|
|
e5fa044eda | ||
|
|
5969120140 | ||
|
|
8801936ad2 | ||
|
|
1d37d46130 | ||
|
|
445c30f3a9 | ||
|
|
5fedea38c2 | ||
|
|
d86549f492 | ||
|
|
4c4eaba7d2 | ||
|
|
cf8f8743bb | ||
|
|
aa75636026 | ||
|
|
2c41b243b6 | ||
|
|
6aea343d4f | ||
|
|
c300e8cbd5 | ||
|
|
6e25c26e9f | ||
|
|
dce1455be7 | ||
|
|
736025b12f | ||
|
|
cb9e9a67a3 | ||
|
|
93c323458f | ||
|
|
6f8c03d8c1 | ||
|
|
afd4228fcf | ||
|
|
d478e5a12e | ||
|
|
0db9ebe67d | ||
|
|
80ea5e6b91 | ||
|
|
cb773babe1 | ||
|
|
b9ed554aca | ||
|
|
f42f3d0e27 | ||
|
|
93ba5b6e5c | ||
|
|
be11d5968e | ||
|
|
0828599e4f | ||
|
|
f47d22c395 | ||
|
|
edf65a62c2 | ||
|
|
12233cb6f6 | ||
|
|
cdce2ac53a | ||
|
|
f4d0371060 | ||
|
|
787a0433cb | ||
|
|
2cf2c13175 | ||
|
|
493e844c01 | ||
|
|
60ea408e51 | ||
|
|
0db0cdfd6c | ||
|
|
26371e5f6b | ||
|
|
6b7c144a11 | ||
|
|
62f43ca24c | ||
|
|
fbf4d3c11e | ||
|
|
7a1a0689b0 | ||
|
|
9ead45d67a | ||
|
|
dfaeda224d | ||
|
|
c0dbe46318 | ||
|
|
abea5e6b5d | ||
|
|
52937c3097 | ||
|
|
597b5bb783 | ||
|
|
d9a1e124f5 | ||
|
|
5f0b7055bf | ||
|
|
1ae6837842 | ||
|
|
252d23bb0e | ||
|
|
d142966d0c | ||
|
|
26cce077bb | ||
|
|
0491bed46d | ||
|
|
16af8bf008 | ||
|
|
064416398b | ||
|
|
ebb7b69dd8 | ||
|
|
e213c76870 | ||
|
|
a80a25a90e | ||
|
|
f8b211be1c | ||
|
|
ab48f118af | ||
|
|
59b0b7321f | ||
|
|
d91e60f7e0 | ||
|
|
9d24aefba1 | ||
|
|
17a429525f | ||
|
|
61543fb10f | ||
|
|
9291950554 | ||
|
|
54689d19ef | ||
|
|
9df586cb59 | ||
|
|
035d7927f9 | ||
|
|
1a4e6de1f4 | ||
|
|
aed73482d1 | ||
|
|
31dbb15448 | ||
|
|
92ac91733e | ||
|
|
29d2d0ec62 | ||
|
|
75df5f8094 | ||
|
|
9ae932823f | ||
|
|
107fe46852 | ||
|
|
63f391ea5f | ||
|
|
035441a492 | ||
|
|
48e62eb1d9 | ||
|
|
b72e037e6a | ||
|
|
de6ed1a0cc | ||
|
|
41c0027391 | ||
|
|
6ce1369a88 | ||
|
|
af9c5c0294 | ||
|
|
d4baddc8d4 | ||
|
|
7ca3b9bd20 | ||
|
|
5ba11ca788 | ||
|
|
d1871b19ee | ||
|
|
54efb6ae4e | ||
|
|
bb3f948596 | ||
|
|
afaf4e62d8 | ||
|
|
5db8f9117f | ||
|
|
27e6668be5 | ||
|
|
6a24b31c6c | ||
|
|
75a7cac783 | ||
|
|
373bc75e98 | ||
|
|
02fd8f22b2 | ||
|
|
4cbfe50fce | ||
|
|
5ba18af021 | ||
|
|
324e7da282 | ||
|
|
8efc38b3eb | ||
|
|
cc3cb6d14f | ||
|
|
77825ee89e | ||
|
|
7625727324 | ||
|
|
47b8c4dd6b | ||
|
|
a63b485b95 | ||
|
|
d1d08963fb |
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
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -3,9 +3,6 @@ on:
|
||||
push:
|
||||
tags: [ v* ]
|
||||
|
||||
env:
|
||||
YAAK_PLUGINS_DIR: checkout/plugins
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
permissions:
|
||||
@@ -65,12 +62,10 @@ jobs:
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: cargo install --force trusted-signing-cli
|
||||
run: cargo install --force trusted-signing-cli --version 0.5.0
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: |
|
||||
npm ci
|
||||
npm install @yaakapp/cli
|
||||
run: npm ci
|
||||
|
||||
- name: Install Protoc for plugin-runtime
|
||||
uses: arduino/setup-protoc@v3
|
||||
@@ -83,12 +78,6 @@ jobs:
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Checkout yaakapp/plugins
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaakapp/plugins
|
||||
path: ${{ env.YAAK_PLUGINS_DIR }}
|
||||
|
||||
- name: Set version
|
||||
run: npm run replace-version
|
||||
env:
|
||||
@@ -96,7 +85,6 @@ jobs:
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
|
||||
@@ -34,8 +34,6 @@ Run the `bootstrap` command to do some initial setup:
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
_NOTE: Run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>` to re-build bundled plugins_
|
||||
|
||||
## Run the App
|
||||
|
||||
After bootstrapping, start the app in development mode:
|
||||
@@ -44,19 +42,21 @@ After bootstrapping, start the app in development mode:
|
||||
npm start
|
||||
```
|
||||
|
||||
_NOTE: If working on bundled plugins, run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>`_
|
||||
|
||||
## SQLite Migrations
|
||||
|
||||
New migrations can be created from the `src-tauri/` directory:
|
||||
|
||||
```shell
|
||||
cd src-tauri
|
||||
sqlx migrate add migration-name
|
||||
npm run migration
|
||||
```
|
||||
|
||||
Run the app to apply the migrations.
|
||||
Rerun the app to apply the migrations.
|
||||
|
||||
If nothing happens, try `cargo clean` and run the app again.
|
||||
_Note: For safety, development builds use a separate database location from production builds._
|
||||
|
||||
_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
|
||||
```
|
||||
|
||||
25
README.md
25
README.md
@@ -3,7 +3,12 @@
|
||||
Yaak is a desktop API client for interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC
|
||||
APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
|
||||

|
||||

|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak is open source, but only accepting contributions for bug fixes. To get started,
|
||||
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
|
||||
|
||||
## Feature Overview
|
||||
|
||||
@@ -14,6 +19,7 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
|
||||
- 📂 Organize requests into workspaces and nested folders.<br/>
|
||||
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
|
||||
- 🛡️ Secure arbitrary text values with end-to-end encryption<br/>
|
||||
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
|
||||
- 🎨 Choose from many of the included themes, or make your own.<br/>
|
||||
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
|
||||
@@ -21,17 +27,8 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
|
||||
- 🛜 Configure a proxy to access firewall-blocked APIs
|
||||
|
||||
## Feedback and Bug Reports
|
||||
## Useful Resources
|
||||
|
||||
All feedback, bug reports, questions, and feature requests should be reported to
|
||||
[feedback.yaak.app](https://feedback.yaak.app).
|
||||
|
||||
## Community Projects
|
||||
|
||||
- [`yaak2postman`](https://github.com/BiteCraft/yaak2postman) CLI for converting Yaak data
|
||||
exports to Postman-compatible collections
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak is open source, but only accepting contributions for bug fixes. To get started,
|
||||
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
|
||||
- [Feedback and Bug Reports](https://feedback.yaak.app)
|
||||
- [Documentation](https://feedback.yaak.app/help)
|
||||
- [Yaak vs Postman](https://yaak.app/blog/postman-alternative)
|
||||
|
||||
7263
package-lock.json
generated
7263
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@@ -10,8 +10,34 @@
|
||||
"packages/plugin-runtime",
|
||||
"packages/plugin-runtime-types",
|
||||
"packages/common-lib",
|
||||
"src-tauri/yaak-license",
|
||||
"plugins/auth-basic",
|
||||
"plugins/auth-bearer",
|
||||
"plugins/auth-jwt",
|
||||
"plugins/auth-oauth2",
|
||||
"plugins/exporter-curl",
|
||||
"plugins/filter-jsonpath",
|
||||
"plugins/filter-xpath",
|
||||
"plugins/importer-curl",
|
||||
"plugins/importer-insomnia",
|
||||
"plugins/importer-openapi",
|
||||
"plugins/importer-postman",
|
||||
"plugins/importer-yaak",
|
||||
"plugins/template-function-cookie",
|
||||
"plugins/template-function-encode",
|
||||
"plugins/template-function-fs",
|
||||
"plugins/template-function-hash",
|
||||
"plugins/template-function-json",
|
||||
"plugins/template-function-prompt",
|
||||
"plugins/template-function-regex",
|
||||
"plugins/template-function-request",
|
||||
"plugins/template-function-response",
|
||||
"plugins/template-function-uuid",
|
||||
"plugins/template-function-xml",
|
||||
"src-tauri/yaak-crypto",
|
||||
"src-tauri/yaak-git",
|
||||
"src-tauri/yaak-fonts",
|
||||
"src-tauri/yaak-license",
|
||||
"src-tauri/yaak-mac-window",
|
||||
"src-tauri/yaak-models",
|
||||
"src-tauri/yaak-plugins",
|
||||
"src-tauri/yaak-sse",
|
||||
@@ -24,7 +50,9 @@
|
||||
"start": "npm run app-dev",
|
||||
"app-build": "tauri build",
|
||||
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
"migration": "node scripts/create-migration.cjs",
|
||||
"build": "npm run --workspaces --if-present build",
|
||||
"build-plugins": "npm run --workspaces --if-present build",
|
||||
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
|
||||
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
|
||||
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
||||
@@ -33,12 +61,16 @@
|
||||
"replace-version": "node scripts/replace-version.cjs",
|
||||
"tauri": "tauri",
|
||||
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
|
||||
"tauri-before-dev": "npm run --workspaces --if-present dev"
|
||||
"tauri-before-dev": "workspaces-run --parallel -- npm run --workspaces --if-present dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"jotai": "^2.12.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"@tauri-apps/cli": "2.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
||||
"@typescript-eslint/parser": "^8.27.0",
|
||||
"@yaakapp/cli": "^0.1.5",
|
||||
"eslint": "^8",
|
||||
"eslint-config-prettier": "^8",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
@@ -48,6 +80,7 @@
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.8.3",
|
||||
"workspaces-run": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/plugin-runtime-types/README.md
Normal file
28
packages/plugin-runtime-types/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Yaak Plugin API
|
||||
|
||||
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
|
||||
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
|
||||
built using Tauri, Rust, and ReactJS.
|
||||
|
||||
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
|
||||
runtime. This package contains the TypeScript type definitions required to make building
|
||||
Yaak plugins a breeze.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The easiest way to get started is by generating a plugin with the Yaak CLI:
|
||||
|
||||
```shell
|
||||
npx @yaakapp/cli generate
|
||||
```
|
||||
|
||||
For more details on creating plugins, check out
|
||||
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
|
||||
|
||||
## Installation
|
||||
|
||||
If you prefer starting from scratch, manually install the types package:
|
||||
|
||||
```shell
|
||||
npm install @yaakapp/api
|
||||
```
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.4",
|
||||
"keywords": [
|
||||
"api-client",
|
||||
"insomnia-alternative",
|
||||
"bruno-alternative",
|
||||
"postman-alternative"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://feedback.yaak.app"
|
||||
},
|
||||
"homepage": "https://yaak.app",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
@@ -20,8 +34,6 @@
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpy-cli": "^5.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.6.2"
|
||||
"cpy-cli": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { HttpResponse } from "./gen_models.js";
|
||||
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
|
||||
import type { JsonValue } from "./serde_json/JsonValue.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
|
||||
export type BootRequest = { dir: string, watch: boolean, };
|
||||
|
||||
@@ -29,7 +23,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
||||
|
||||
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
||||
|
||||
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
|
||||
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonValue }, };
|
||||
|
||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||
|
||||
@@ -104,7 +98,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputCheckbox = {
|
||||
/**
|
||||
@@ -131,7 +129,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputEditor = {
|
||||
/**
|
||||
@@ -170,7 +172,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputFile = {
|
||||
/**
|
||||
@@ -205,7 +211,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
@@ -232,7 +242,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||
|
||||
@@ -265,7 +279,11 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputSelectOption = { label: string, value: string, };
|
||||
|
||||
@@ -306,10 +324,18 @@ hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean, };
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
|
||||
|
||||
export type GetCookieValueRequest = { name: string, };
|
||||
|
||||
export type GetCookieValueResponse = { value: string | null, };
|
||||
|
||||
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
|
||||
|
||||
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
|
||||
@@ -344,18 +370,24 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
|
||||
|
||||
export type ImportResponse = { resources: ImportResources, };
|
||||
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
export type ListCookieNamesRequest = {};
|
||||
|
||||
export type ListCookieNamesResponse = { names: Array<string>, };
|
||||
|
||||
export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* Label for the window. If not provided, a random one will be generated.
|
||||
*/
|
||||
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
||||
|
||||
export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, };
|
||||
|
||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||
/**
|
||||
* Text to add to the confirmation button
|
||||
@@ -393,14 +425,17 @@ export type TemplateFunction = { name: string, description?: string,
|
||||
* Also support alternative names. This is useful for not breaking existing
|
||||
* tags when changing the `name` property
|
||||
*/
|
||||
aliases?: Array<string>, args: Array<FormInput>, };
|
||||
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
|
||||
|
||||
/**
|
||||
* Similar to FormInput, but contains
|
||||
*/
|
||||
export type TemplateFunctionArg = FormInput;
|
||||
|
||||
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
|
||||
|
||||
export type TemplateRenderResponse = { data: JsonValue, };
|
||||
|
||||
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
|
||||
|
||||
export type WindowNavigateEvent = { url: string, };
|
||||
|
||||
export type WindowSize = { width: number, height: number, };
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -1,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 JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };
|
||||
export type JsonValue = number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null;
|
||||
|
||||
@@ -3,3 +3,7 @@ export type * from './themes';
|
||||
|
||||
export * from './bindings/gen_models';
|
||||
export * from './bindings/gen_events';
|
||||
|
||||
// Some extras for utility
|
||||
|
||||
export type { PartialImportResources } from './plugins/ImporterPlugin';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type {
|
||||
FindHttpResponsesRequest,
|
||||
FindHttpResponsesResponse,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdRequest,
|
||||
GetHttpRequestByIdResponse,
|
||||
ListCookieNamesResponse,
|
||||
OpenWindowRequest,
|
||||
PromptTextRequest,
|
||||
PromptTextResponse,
|
||||
@@ -34,10 +37,14 @@ export interface Context {
|
||||
openUrl(
|
||||
args: OpenWindowRequest & {
|
||||
onNavigate?: (args: { url: string }) => void;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
},
|
||||
): Promise<{ close: () => void }>;
|
||||
};
|
||||
cookies: {
|
||||
listNames(): Promise<ListCookieNamesResponse['names']>;
|
||||
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
|
||||
};
|
||||
httpRequest: {
|
||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Context } from './Context';
|
||||
|
||||
export type FilterPluginResponse = { filtered: string };
|
||||
type FilterPluginResponse = { filtered: string };
|
||||
|
||||
export type FilterPlugin = {
|
||||
name: string;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../bindings/gen_models';
|
||||
import type { AtLeast } from '../helpers';
|
||||
import { ImportResources } from '../bindings/gen_events';
|
||||
import { AtLeast } from '../helpers';
|
||||
import type { Context } from './Context';
|
||||
|
||||
type ImportPluginResponse = null | {
|
||||
resources: {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
};
|
||||
type RootFields = 'name' | 'id' | 'model';
|
||||
type CommonFields = RootFields | 'workspaceId';
|
||||
|
||||
export type PartialImportResources = {
|
||||
workspaces: Array<AtLeast<ImportResources['workspaces'][0], RootFields>>;
|
||||
environments: Array<AtLeast<ImportResources['environments'][0], CommonFields>>;
|
||||
folders: Array<AtLeast<ImportResources['folders'][0], CommonFields>>;
|
||||
httpRequests: Array<AtLeast<ImportResources['httpRequests'][0], CommonFields>>;
|
||||
grpcRequests: Array<AtLeast<ImportResources['grpcRequests'][0], CommonFields>>;
|
||||
websocketRequests: Array<AtLeast<ImportResources['websocketRequests'][0], CommonFields>>;
|
||||
};
|
||||
|
||||
export type ImportPluginResponse = null | {
|
||||
resources: PartialImportResources;
|
||||
};
|
||||
|
||||
export type ImporterPlugin = {
|
||||
|
||||
@@ -15,7 +15,6 @@ export class PluginHandle {
|
||||
bootRequest: this.bootRequest,
|
||||
};
|
||||
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
|
||||
console.log('Created plugin worker for ', this.bootRequest.dir);
|
||||
}
|
||||
|
||||
sendToWorker(event: InternalEvent) {
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import type {
|
||||
import {
|
||||
BootRequest,
|
||||
Context,
|
||||
DeleteKeyValueResponse,
|
||||
FindHttpResponsesResponse,
|
||||
FormInput,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdResponse,
|
||||
GetKeyValueResponse,
|
||||
HttpAuthenticationAction,
|
||||
HttpRequestAction,
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
ListCookieNamesResponse,
|
||||
PluginWindowContext,
|
||||
PromptTextResponse,
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
TemplateFunctionArg,
|
||||
TemplateRenderResponse,
|
||||
WindowContext,
|
||||
} from '@yaakapp/api';
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
|
||||
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 {
|
||||
@@ -45,12 +46,12 @@ export class PluginInstance {
|
||||
this.#appToPluginEvents = new EventChannel();
|
||||
|
||||
// Forward incoming events to onMessage()
|
||||
this.#appToPluginEvents.listen(async event => {
|
||||
this.#appToPluginEvents.listen(async (event) => {
|
||||
await this.#onMessage(event);
|
||||
})
|
||||
});
|
||||
|
||||
// Reload plugin if the JS or package.json changes
|
||||
const windowContextNone: WindowContext = { type: 'none' };
|
||||
const windowContextNone: PluginWindowContext = { type: 'none' };
|
||||
const fileChangeCallback = async () => {
|
||||
this.#importModule();
|
||||
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
||||
@@ -261,10 +262,10 @@ export class PluginInstance {
|
||||
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);
|
||||
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (typeof fn?.onRender === 'function') {
|
||||
applyFormInputDefaults(fn.args, payload.args.values);
|
||||
const result = await fn.onRender(ctx, payload.args);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
@@ -281,15 +282,9 @@ export class PluginInstance {
|
||||
this.#importModule();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Plugin call threw exception', payload.type, err);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'error_response',
|
||||
error: `${err}`,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
const error = `${err}`.replace(/^Error:\s*/g, '');
|
||||
console.log('Plugin call threw exception', payload.type, '→', error);
|
||||
this.#sendPayload(windowContext, { type: 'error_response', error }, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -317,7 +312,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#buildEventToSend(
|
||||
windowContext: WindowContext,
|
||||
windowContext: PluginWindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null = null,
|
||||
): InternalEvent {
|
||||
@@ -332,7 +327,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#sendPayload(
|
||||
windowContext: WindowContext,
|
||||
windowContext: PluginWindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null,
|
||||
): string {
|
||||
@@ -348,12 +343,12 @@ export class PluginInstance {
|
||||
this.#pluginToAppEvents.emit(event);
|
||||
}
|
||||
|
||||
#sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
|
||||
#sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string {
|
||||
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
|
||||
}
|
||||
|
||||
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||
windowContext: WindowContext,
|
||||
windowContext: PluginWindowContext,
|
||||
payload: InternalEventPayload,
|
||||
): Promise<T> {
|
||||
// 1. Build event to send
|
||||
@@ -379,7 +374,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#sendAndListenForEvents(
|
||||
windowContext: WindowContext,
|
||||
windowContext: PluginWindowContext,
|
||||
payload: InternalEventPayload,
|
||||
onEvent: (event: InternalEventPayload) => void,
|
||||
): void {
|
||||
@@ -495,6 +490,27 @@ export class PluginInstance {
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
cookies: {
|
||||
getValue: async (args: GetCookieValueRequest) => {
|
||||
const payload = {
|
||||
type: 'get_cookie_value_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return value;
|
||||
},
|
||||
listNames: async () => {
|
||||
const payload = { type: 'list_cookie_names_request' } as const;
|
||||
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return names;
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
/**
|
||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||
@@ -551,8 +567,8 @@ function genId(len = 5): string {
|
||||
|
||||
/** Recursively apply form input defaults to a set of values */
|
||||
function applyFormInputDefaults(
|
||||
inputs: FormInput[],
|
||||
values: { [p: string]: JsonPrimitive | undefined },
|
||||
inputs: TemplateFunctionArg[],
|
||||
values: { [p: string]: JsonValue | undefined },
|
||||
) {
|
||||
for (const input of inputs) {
|
||||
if ('inputs' in input) {
|
||||
@@ -580,18 +596,3 @@ function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||
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();
|
||||
// });
|
||||
// }
|
||||
|
||||
1
plugins/.gitignore
vendored
Normal file
1
plugins/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*/build
|
||||
26
plugins/auth-basic/src/index.ts
Normal file
26
plugins/auth-basic/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'basic',
|
||||
label: 'Basic Auth',
|
||||
shortLabel: 'Basic',
|
||||
args: [{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
optional: true,
|
||||
}, {
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
optional: true,
|
||||
password: true,
|
||||
}],
|
||||
async onApply(_ctx, { values }) {
|
||||
const { username, password } = values;
|
||||
const value = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
||||
return { setHeaders: [{ name: 'Authorization', value }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
21
plugins/auth-bearer/src/index.ts
Normal file
21
plugins/auth-bearer/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'bearer',
|
||||
label: 'Bearer Token',
|
||||
shortLabel: 'Bearer',
|
||||
args: [{
|
||||
type: 'text',
|
||||
name: 'token',
|
||||
label: 'Token',
|
||||
optional: true,
|
||||
password: true,
|
||||
}],
|
||||
async onApply(_ctx, { values }) {
|
||||
const { token } = values;
|
||||
const value = `Bearer ${token}`.trim();
|
||||
return { setHeaders: [{ name: 'Authorization', value }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
68
plugins/auth-jwt/src/index.ts
Normal file
68
plugins/auth-jwt/src/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PluginDefinition } from '@yaakapp/api';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const algorithms = [
|
||||
'HS256',
|
||||
'HS384',
|
||||
'HS512',
|
||||
'RS256',
|
||||
'RS384',
|
||||
'RS512',
|
||||
'PS256',
|
||||
'PS384',
|
||||
'PS512',
|
||||
'ES256',
|
||||
'ES384',
|
||||
'ES512',
|
||||
'none',
|
||||
] as const;
|
||||
|
||||
const defaultAlgorithm = algorithms[0];
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'jwt',
|
||||
label: 'JWT Bearer',
|
||||
shortLabel: 'JWT',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'algorithm',
|
||||
label: 'Algorithm',
|
||||
hideLabel: true,
|
||||
defaultValue: defaultAlgorithm,
|
||||
options: algorithms.map(value => ({ label: value === 'none' ? 'None' : value, value })),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'secret',
|
||||
label: 'Secret or Private Key',
|
||||
password: true,
|
||||
optional: true,
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'secretBase64',
|
||||
label: 'Secret is base64 encoded',
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'payload',
|
||||
label: 'Payload',
|
||||
language: 'json',
|
||||
defaultValue: '{\n "foo": "bar"\n}',
|
||||
placeholder: '{ }',
|
||||
},
|
||||
],
|
||||
async onApply(_ctx, { values }) {
|
||||
const { algorithm, secret: _secret, secretBase64, payload } = values;
|
||||
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
|
||||
const token = jwt.sign(`${payload}`, secret, { algorithm: algorithm as any });
|
||||
const value = `Bearer ${token}`;
|
||||
return { setHeaders: [{ name: 'Authorization', value }] };
|
||||
}
|
||||
,
|
||||
},
|
||||
}
|
||||
;
|
||||
78
plugins/auth-oauth2/src/getAccessToken.ts
Normal file
78
plugins/auth-oauth2/src/getAccessToken.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { AccessTokenRawResponse } from './store';
|
||||
|
||||
export async function getAccessToken(
|
||||
ctx: Context,
|
||||
{
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
audience,
|
||||
params,
|
||||
grantType,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
}: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
grantType: string;
|
||||
accessTokenUrl: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
params: HttpUrlParameter[];
|
||||
},
|
||||
): Promise<AccessTokenRawResponse> {
|
||||
console.log('[oauth2] Getting access token', accessTokenUrl);
|
||||
const httpRequest: Partial<HttpRequest> = {
|
||||
method: 'POST',
|
||||
url: accessTokenUrl,
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: [{ name: 'grant_type', value: grantType }, ...params],
|
||||
},
|
||||
headers: [
|
||||
{ name: 'User-Agent', value: 'yaak' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
],
|
||||
};
|
||||
|
||||
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
|
||||
if (audience) httpRequest.body!.form.push({ name: 'audience', value: audience });
|
||||
|
||||
if (credentialsInBody) {
|
||||
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
|
||||
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
|
||||
} else {
|
||||
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
||||
httpRequest.headers!.push({ name: 'Authorization', value });
|
||||
}
|
||||
|
||||
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
|
||||
console.log('[oauth2] Got access token response', resp.status);
|
||||
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(
|
||||
'Failed to fetch access token with status=' + resp.status + ' and body=' + body,
|
||||
);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch {
|
||||
response = Object.fromEntries(new URLSearchParams(body));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error('Failed to fetch access token with ' + response.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
103
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
103
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Context, HttpRequest } from '@yaakapp/api';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store';
|
||||
|
||||
export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
|
||||
scope,
|
||||
accessTokenUrl,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
forceRefresh,
|
||||
}: {
|
||||
scope: string | null;
|
||||
accessTokenUrl: string;
|
||||
credentialsInBody: boolean;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
forceRefresh?: boolean;
|
||||
}): Promise<AccessToken | null> {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const isExpired = token.expiresAt && now > token.expiresAt;
|
||||
|
||||
// Return the current access token if it's still valid
|
||||
if (!isExpired && !forceRefresh) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token is expired, but there's no refresh token :(
|
||||
if (!token.response.refresh_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Access token is expired, so get a new one
|
||||
const httpRequest: Partial<HttpRequest> = {
|
||||
method: 'POST',
|
||||
url: accessTokenUrl,
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'grant_type', value: 'refresh_token' },
|
||||
{ name: 'refresh_token', value: token.response.refresh_token },
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{ name: 'User-Agent', value: 'yaak' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
],
|
||||
};
|
||||
|
||||
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
|
||||
|
||||
if (credentialsInBody) {
|
||||
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
|
||||
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
|
||||
} else {
|
||||
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
||||
httpRequest.headers!.push({ name: 'Authorization', value });
|
||||
}
|
||||
|
||||
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
|
||||
if (resp.status === 401) {
|
||||
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
|
||||
// and returning null;
|
||||
console.log('[oauth2] Unauthorized refresh_token request');
|
||||
await deleteToken(ctx, contextId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
|
||||
|
||||
console.log('[oauth2] Got refresh token response', resp.status);
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error('Failed to refresh access token with status=' + resp.status + ' and body=' + body);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch {
|
||||
response = Object.fromEntries(new URLSearchParams(body));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
|
||||
}
|
||||
|
||||
const newResponse: AccessTokenRawResponse = {
|
||||
...response,
|
||||
// Assign a new one or keep the old one,
|
||||
refresh_token: response.refresh_token ?? token.response.refresh_token,
|
||||
};
|
||||
|
||||
return storeToken(ctx, contextId, newResponse);
|
||||
}
|
||||
152
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
152
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import { AccessToken, getDataDirKey, storeToken } from '../store';
|
||||
|
||||
export const PKCE_SHA256 = 'S256';
|
||||
export const PKCE_PLAIN = 'plain';
|
||||
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
||||
|
||||
export async function getAuthorizationCode(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
pkce,
|
||||
tokenName,
|
||||
}: {
|
||||
authorizationUrl: string;
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectUri: string | null;
|
||||
scope: string | null;
|
||||
state: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
pkce: {
|
||||
challengeMethod: string | null;
|
||||
codeVerifier: string | null;
|
||||
} | null;
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody,
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
if (pkce) {
|
||||
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
|
||||
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
|
||||
authorizationUrl.searchParams.set(
|
||||
'code_challenge',
|
||||
createPkceCodeChallenge(verifier, challengeMethod),
|
||||
);
|
||||
authorizationUrl.searchParams.set('code_challenge_method', challengeMethod);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
|
||||
console.log('[oauth2] 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 (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
|
||||
|
||||
if (url.searchParams.has('error')) {
|
||||
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
|
||||
}
|
||||
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
console.log('[oauth2] Code not found');
|
||||
return; // Could be one of many redirects in a chain, so skip it
|
||||
}
|
||||
|
||||
// Close the window here, because we don't need it anymore!
|
||||
foundCode = true;
|
||||
close();
|
||||
|
||||
console.log('[oauth2] Code found');
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'code', value: code },
|
||||
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response, tokenName));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createPkceCodeVerifier() {
|
||||
return encodeForPkce(randomBytes(32));
|
||||
}
|
||||
|
||||
function createPkceCodeChallenge(verifier: string, method: string) {
|
||||
if (method === 'plain') {
|
||||
return verifier;
|
||||
}
|
||||
|
||||
const hash = encodeForPkce(createHash('sha256').update(verifier).digest());
|
||||
return hash
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
}
|
||||
|
||||
function encodeForPkce(bytes: Buffer) {
|
||||
return bytes
|
||||
.toString('base64')
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
}
|
||||
43
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
43
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getToken, storeToken } from '../store';
|
||||
|
||||
export async function getClientCredentials(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
}: {
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
// resolve(token.response.access_token);
|
||||
// TODO: Refresh token if expired
|
||||
// return;
|
||||
}
|
||||
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsInBody,
|
||||
params: [],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
86
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
86
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store';
|
||||
|
||||
export function getImplicit(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
responseType,
|
||||
clientId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
tokenName,
|
||||
}: {
|
||||
authorizationUrl: string;
|
||||
responseType: string;
|
||||
clientId: string;
|
||||
redirectUri: string | null;
|
||||
scope: string | null;
|
||||
state: string | null;
|
||||
audience: string | null;
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
// resolve(token.response.access_token);
|
||||
// TODO: Refresh token if expired
|
||||
// return;
|
||||
}
|
||||
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl.searchParams.set('response_type', 'token');
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
if (responseType.includes('id_token')) {
|
||||
authorizationUrl.searchParams.set(
|
||||
'nonce',
|
||||
String(Math.floor(Math.random() * 9999999999999) + 1),
|
||||
);
|
||||
}
|
||||
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
let foundAccessToken = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: 'oauth-authorization-url',
|
||||
async onClose() {
|
||||
if (!foundAccessToken) {
|
||||
reject(new Error('Authorization window closed'));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has('error')) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get('error')}`));
|
||||
}
|
||||
|
||||
const hash = url.hash.slice(1);
|
||||
const params = new URLSearchParams(hash);
|
||||
|
||||
const accessToken = params.get(tokenName);
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
foundAccessToken = true;
|
||||
|
||||
// Close the window here, because we don't need it anymore
|
||||
close();
|
||||
|
||||
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
55
plugins/auth-oauth2/src/grants/password.ts
Normal file
55
plugins/auth-oauth2/src/grants/password.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import { AccessToken, storeToken } from '../store';
|
||||
|
||||
export async function getPassword(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
credentialsInBody,
|
||||
audience,
|
||||
scope,
|
||||
}: {
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody,
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const response = await getAccessToken(ctx, {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
grantType: 'password',
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'username', value: username },
|
||||
{ name: 'password', value: password },
|
||||
],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
404
plugins/auth-oauth2/src/index.ts
Normal file
404
plugins/auth-oauth2/src/index.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import {
|
||||
Context,
|
||||
FormInputSelectOption,
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
import {
|
||||
DEFAULT_PKCE_METHOD,
|
||||
getAuthorizationCode,
|
||||
PKCE_PLAIN,
|
||||
PKCE_SHA256,
|
||||
} from './grants/authorizationCode';
|
||||
import { getClientCredentials } from './grants/clientCredentials';
|
||||
import { getImplicit } from './grants/implicit';
|
||||
import { getPassword } from './grants/password';
|
||||
import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store';
|
||||
|
||||
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
|
||||
|
||||
const grantTypes: FormInputSelectOption[] = [
|
||||
{ label: 'Authorization Code', value: 'authorization_code' },
|
||||
{ label: 'Implicit', value: 'implicit' },
|
||||
{ label: 'Resource Owner Password Credential', value: 'password' },
|
||||
{ label: 'Client Credentials', value: 'client_credentials' },
|
||||
];
|
||||
|
||||
const defaultGrantType = grantTypes[0]!.value;
|
||||
|
||||
function hiddenIfNot(
|
||||
grantTypes: GrantType[],
|
||||
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
|
||||
) {
|
||||
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
|
||||
const hasGrantType = grantTypes.find((t) => t === String(values.grantType ?? defaultGrantType));
|
||||
const hasOtherBools = other.every((t) => t(values));
|
||||
const show = hasGrantType && hasOtherBools;
|
||||
return { hidden: !show };
|
||||
};
|
||||
}
|
||||
|
||||
const authorizationUrls = [
|
||||
'https://github.com/login/oauth/authorize',
|
||||
'https://account.box.com/api/oauth2/authorize',
|
||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
'https://api.imgur.com/oauth2/authorize',
|
||||
'https://bitly.com/oauth/authorize',
|
||||
'https://gitlab.example.com/oauth/authorize',
|
||||
'https://medium.com/m/oauth/authorize',
|
||||
'https://public-api.wordpress.com/oauth2/authorize',
|
||||
'https://slack.com/oauth/authorize',
|
||||
'https://todoist.com/oauth/authorize',
|
||||
'https://www.dropbox.com/oauth2/authorize',
|
||||
'https://www.linkedin.com/oauth/v2/authorization',
|
||||
'https://MY_SHOP.myshopify.com/admin/oauth/access_token',
|
||||
'https://appcenter.intuit.com/app/connect/oauth2/authorize',
|
||||
];
|
||||
|
||||
const accessTokenUrls = [
|
||||
'https://github.com/login/oauth/access_token',
|
||||
'https://api-ssl.bitly.com/oauth/access_token',
|
||||
'https://api.box.com/oauth2/token',
|
||||
'https://api.dropboxapi.com/oauth2/token',
|
||||
'https://api.imgur.com/oauth2/token',
|
||||
'https://api.medium.com/v1/tokens',
|
||||
'https://gitlab.example.com/oauth/token',
|
||||
'https://public-api.wordpress.com/oauth2/token',
|
||||
'https://slack.com/api/oauth.access',
|
||||
'https://todoist.com/oauth/access_token',
|
||||
'https://www.googleapis.com/oauth2/v4/token',
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
'https://MY_SHOP.myshopify.com/admin/oauth/authorize',
|
||||
'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
|
||||
];
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'oauth2',
|
||||
label: 'OAuth 2.0',
|
||||
shortLabel: 'OAuth 2',
|
||||
actions: [
|
||||
{
|
||||
label: 'Copy Current Token',
|
||||
async onSelect(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
|
||||
} else {
|
||||
await ctx.clipboard.copyText(token.response.access_token);
|
||||
await ctx.toast.show({
|
||||
message: 'Token copied to clipboard',
|
||||
icon: 'copy',
|
||||
color: 'success',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete Token',
|
||||
async onSelect(ctx, { contextId }) {
|
||||
if (await deleteToken(ctx, contextId)) {
|
||||
await ctx.toast.show({ message: 'Token deleted', color: 'success' });
|
||||
} else {
|
||||
await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clear Window Session',
|
||||
async onSelect(ctx, { contextId }) {
|
||||
await resetDataDirKey(ctx, contextId);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Toggle Debug Logs',
|
||||
async onSelect(ctx) {
|
||||
const enableLogs = !(await ctx.store.get('enable_logs'));
|
||||
await ctx.store.set('enable_logs', enableLogs);
|
||||
await ctx.toast.show({
|
||||
message: `Debug logs ${enableLogs ? 'enabled' : 'disabled'}`,
|
||||
color: 'info',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'grantType',
|
||||
label: 'Grant Type',
|
||||
hideLabel: true,
|
||||
defaultValue: defaultGrantType,
|
||||
options: grantTypes,
|
||||
},
|
||||
|
||||
// Always-present fields
|
||||
{
|
||||
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],
|
||||
completionOptions: authorizationUrls.map((url) => ({ label: url, value: url })),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'accessTokenUrl',
|
||||
optional: true,
|
||||
label: 'Access Token URL',
|
||||
placeholder: accessTokenUrls[0],
|
||||
dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']),
|
||||
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url })),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'redirectUri',
|
||||
label: 'Redirect URI',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'audience',
|
||||
label: 'Audience',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'tokenName',
|
||||
label: 'Token for authorization',
|
||||
description:
|
||||
'Select which token to send in the "Authorization: Bearer" header. Most APIs expect ' +
|
||||
'access_token, but some (like OpenID Connect) require id_token.',
|
||||
defaultValue: 'access_token',
|
||||
options: [
|
||||
{ label: 'access_token', value: 'access_token' },
|
||||
{ label: 'id_token', value: 'id_token' },
|
||||
],
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'usePkce',
|
||||
label: 'Use PKCE',
|
||||
dynamic: hiddenIfNot(['authorization_code']),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'pkceChallengeMethod',
|
||||
label: 'Code Challenge Method',
|
||||
options: [
|
||||
{ label: 'SHA-256', value: PKCE_SHA256 },
|
||||
{ label: 'Plain', value: PKCE_PLAIN },
|
||||
],
|
||||
defaultValue: DEFAULT_PKCE_METHOD,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pkceCodeVerifier',
|
||||
label: 'Code Verifier',
|
||||
placeholder: 'Automatically generated if not provided',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
password: true,
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'responseType',
|
||||
label: 'Response Type',
|
||||
defaultValue: 'token',
|
||||
options: [
|
||||
{ label: 'Access Token', value: 'token' },
|
||||
{ label: 'ID Token', value: 'id_token' },
|
||||
{ label: 'ID and Access Token', value: 'id_token token' },
|
||||
],
|
||||
dynamic: hiddenIfNot(['implicit']),
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
inputs: [
|
||||
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerPrefix',
|
||||
label: 'Header Prefix',
|
||||
optional: true,
|
||||
defaultValue: 'Bearer',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'credentials',
|
||||
label: 'Send Credentials',
|
||||
defaultValue: 'body',
|
||||
options: [
|
||||
{ label: 'In Request Body', value: 'body' },
|
||||
{ label: 'As Basic Authentication', value: 'basic' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Access Token Response',
|
||||
async dynamic(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return { hidden: true };
|
||||
}
|
||||
return {
|
||||
label: 'Access Token Response',
|
||||
inputs: [
|
||||
{
|
||||
type: 'editor',
|
||||
defaultValue: JSON.stringify(token.response, null, 2),
|
||||
hideLabel: true,
|
||||
readOnly: true,
|
||||
language: 'json',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
async onApply(ctx, { values, contextId }) {
|
||||
const headerPrefix = stringArg(values, 'headerPrefix');
|
||||
const grantType = stringArg(values, 'grantType') as GrantType;
|
||||
const credentialsInBody = values.credentials === 'body';
|
||||
const tokenName = values.tokenName === 'id_token' ? 'id_token' : 'access_token';
|
||||
|
||||
let token: AccessToken;
|
||||
if (grantType === 'authorization_code') {
|
||||
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: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
state: stringArgOrNull(values, 'state'),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce
|
||||
? {
|
||||
challengeMethod: stringArg(values, 'pkceChallengeMethod'),
|
||||
codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'),
|
||||
}
|
||||
: null,
|
||||
tokenName: tokenName,
|
||||
});
|
||||
} else if (grantType === 'implicit') {
|
||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||
token = await getImplicit(ctx, contextId, {
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//)
|
||||
? authorizationUrl
|
||||
: `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
responseType: stringArg(values, 'responseType'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
state: stringArgOrNull(values, 'state'),
|
||||
tokenName: tokenName,
|
||||
});
|
||||
} else if (grantType === 'client_credentials') {
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
token = await getClientCredentials(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
credentialsInBody,
|
||||
});
|
||||
} else if (grantType === 'password') {
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
token = await getPassword(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
username: stringArg(values, 'username'),
|
||||
password: stringArg(values, 'password'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
credentialsInBody,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Invalid grant type ' + grantType);
|
||||
}
|
||||
|
||||
const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim();
|
||||
return {
|
||||
setHeaders: [
|
||||
{
|
||||
name: 'Authorization',
|
||||
value: headerValue,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function stringArgOrNull(
|
||||
values: Record<string, JsonPrimitive | undefined>,
|
||||
name: string,
|
||||
): string | null {
|
||||
const arg = values[name];
|
||||
if (arg == null || arg == '') return null;
|
||||
return `${arg}`;
|
||||
}
|
||||
|
||||
function stringArg(values: Record<string, JsonPrimitive | undefined>, name: string): string {
|
||||
const arg = stringArgOrNull(values, name);
|
||||
if (!arg) return '';
|
||||
return arg;
|
||||
}
|
||||
62
plugins/auth-oauth2/src/store.ts
Normal file
62
plugins/auth-oauth2/src/store.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
|
||||
export async function storeToken(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
response: AccessTokenRawResponse,
|
||||
tokenName: 'access_token' | 'id_token' = 'access_token',
|
||||
) {
|
||||
if (!response[tokenName]) {
|
||||
throw new Error(`${tokenName} not found in response ${Object.keys(response).join(', ')}`);
|
||||
}
|
||||
|
||||
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1000 : null;
|
||||
const token: AccessToken = {
|
||||
response,
|
||||
expiresAt,
|
||||
};
|
||||
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function getToken(ctx: Context, contextId: string) {
|
||||
return ctx.store.get<AccessToken>(tokenStoreKey(contextId));
|
||||
}
|
||||
|
||||
export async function deleteToken(ctx: Context, contextId: string) {
|
||||
return ctx.store.delete(tokenStoreKey(contextId));
|
||||
}
|
||||
|
||||
export async function resetDataDirKey(ctx: Context, contextId: string) {
|
||||
const key = new Date().toISOString();
|
||||
return ctx.store.set<string>(dataDirStoreKey(contextId), key);
|
||||
}
|
||||
|
||||
export async function getDataDirKey(ctx: Context, contextId: string) {
|
||||
const key = (await ctx.store.get<string>(dataDirStoreKey(contextId))) ?? 'default';
|
||||
return `${contextId}::${key}`;
|
||||
}
|
||||
|
||||
function tokenStoreKey(contextId: string) {
|
||||
return ['token', contextId].join('::');
|
||||
}
|
||||
|
||||
function dataDirStoreKey(contextId: string) {
|
||||
return ['data_dir', contextId].join('::');
|
||||
}
|
||||
|
||||
export interface AccessToken {
|
||||
response: AccessTokenRawResponse;
|
||||
expiresAt: number | null;
|
||||
}
|
||||
|
||||
export interface AccessTokenRawResponse {
|
||||
access_token: string;
|
||||
id_token?: string;
|
||||
token_type?: string;
|
||||
expires_in?: number;
|
||||
refresh_token?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
scope?: string;
|
||||
}
|
||||
101
plugins/exporter-curl/src/index.ts
Normal file
101
plugins/exporter-curl/src/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { HttpRequest, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
const NEWLINE = '\\\n ';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
httpRequestActions: [{
|
||||
label: 'Copy as Curl',
|
||||
icon: 'copy',
|
||||
async onSelect(ctx, args) {
|
||||
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: 'preview' });
|
||||
const data = await convertToCurl(rendered_request);
|
||||
await ctx.clipboard.copyText(data);
|
||||
await ctx.toast.show({ message: 'Curl copied to clipboard', icon: 'copy', color: 'success' });
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
export async function convertToCurl(request: Partial<HttpRequest>) {
|
||||
const xs = ['curl'];
|
||||
|
||||
// Add method and URL all on first line
|
||||
if (request.method) xs.push('-X', request.method);
|
||||
if (request.url) xs.push(quote(request.url));
|
||||
|
||||
|
||||
xs.push(NEWLINE);
|
||||
|
||||
// Add URL params
|
||||
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
|
||||
xs.push('--url-query', quote(`${p.name}=${p.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add headers
|
||||
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
|
||||
xs.push('--header', quote(`${h.name}: ${h.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add form params
|
||||
if (Array.isArray(request.body?.form)) {
|
||||
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
|
||||
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
|
||||
if (p.file) {
|
||||
let v = `${p.name}=@${p.file}`;
|
||||
v += p.contentType ? `;type=${p.contentType}` : '';
|
||||
xs.push(flag, v);
|
||||
} else {
|
||||
xs.push(flag, quote(`${p.name}=${p.value}`));
|
||||
}
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
} else if (typeof request.body?.query === 'string') {
|
||||
const body = { query: request.body.query || '', variables: maybeParseJSON(request.body.variables, undefined) };
|
||||
xs.push('--data-raw', `${quote(JSON.stringify(body))}`);
|
||||
xs.push(NEWLINE);
|
||||
} else if (typeof request.body?.text === 'string') {
|
||||
xs.push('--data-raw', `${quote(request.body.text)}`);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add basic/digest authentication
|
||||
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
|
||||
if (request.authenticationType === 'digest') xs.push('--digest');
|
||||
xs.push(
|
||||
'--user',
|
||||
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),
|
||||
);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add bearer authentication
|
||||
if (request.authenticationType === 'bearer') {
|
||||
xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Remove trailing newline
|
||||
if (xs[xs.length - 1] === NEWLINE) {
|
||||
xs.splice(xs.length - 1, 1);
|
||||
}
|
||||
|
||||
return xs.join(' ');
|
||||
}
|
||||
|
||||
function quote(arg: string): string {
|
||||
const escaped = arg.replace(/'/g, '\\\'');
|
||||
return `'${escaped}'`;
|
||||
}
|
||||
|
||||
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
|
||||
return v.enabled !== false && !!v.name;
|
||||
}
|
||||
|
||||
function maybeParseJSON(v: any, fallback: any): string {
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
206
plugins/exporter-curl/tests/index.test.ts
Normal file
206
plugins/exporter-curl/tests/index.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { convertToCurl } from '../src';
|
||||
|
||||
describe('exporter-curl', () => {
|
||||
test('Exports GET with params', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
test('Exports POST with url form data', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports POST with GraphQL data', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'graphql',
|
||||
body: {
|
||||
query: '{foo,bar}',
|
||||
variables: '{"a": "aaa", "b": "bbb"}',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports POST with GraphQL data no variables', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'graphql',
|
||||
body: {
|
||||
query: '{foo,bar}',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}"}'`].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports PUT with multipart form', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'PUT',
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: 'f', file: '/foo/bar.png', contentType: 'image/png' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X PUT 'https://yaak.app'`,
|
||||
`--form 'a=aaa'`,
|
||||
`--form 'b=bbb'`,
|
||||
`--form f=@/foo/bar.png;type=image/png`,
|
||||
].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports JSON body', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/json',
|
||||
body: {
|
||||
text: `{"foo":"bar's"}`,
|
||||
},
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--header 'Content-Type: application/json'`,
|
||||
`--data-raw '{"foo":"bar\\'s"}'`,
|
||||
].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports multi-line JSON body', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/json',
|
||||
body: {
|
||||
text: `{"foo":"bar",\n"baz":"qux"}`,
|
||||
},
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--header 'Content-Type: application/json'`,
|
||||
`--data-raw '{"foo":"bar",\n"baz":"qux"}'`,
|
||||
].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports headers', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
headers: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
],
|
||||
}),
|
||||
).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
|
||||
});
|
||||
|
||||
test('Basic auth', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `));
|
||||
});
|
||||
|
||||
test('Broken basic auth', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
authentication: {},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
|
||||
});
|
||||
|
||||
test('Digest auth', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'digest',
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `));
|
||||
});
|
||||
|
||||
test('Bearer auth', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
authentication: {
|
||||
token: 'tok',
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
|
||||
});
|
||||
|
||||
test('Broken bearer auth', async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^9.0.0"
|
||||
"jsonpath-plus": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
14
plugins/filter-jsonpath/src/index.ts
Normal file
14
plugins/filter-jsonpath/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
filter: {
|
||||
name: 'JSONPath',
|
||||
description: 'Filter JSONPath',
|
||||
onFilter(_ctx, args) {
|
||||
const parsed = JSON.parse(args.payload);
|
||||
const filtered = JSONPath({ path: args.filter, json: parsed });
|
||||
return { filtered: JSON.stringify(filtered, null, 2) };
|
||||
},
|
||||
},
|
||||
};
|
||||
21
plugins/filter-xpath/src/index.ts
Normal file
21
plugins/filter-xpath/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import { PluginDefinition } from '@yaakapp/api';
|
||||
import xpath from 'xpath';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
filter: {
|
||||
name: 'XPath',
|
||||
description: 'Filter XPath',
|
||||
onFilter(_ctx, args) {
|
||||
const doc = new DOMParser().parseFromString(args.payload, 'text/xml');
|
||||
const result = xpath.select(args.filter, doc, false);
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return { filtered: result.map(r => String(r)).join('\n') };
|
||||
} else {
|
||||
// Not sure what cases this happens in (?)
|
||||
return { filtered: String(result) };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
441
plugins/importer-curl/src/index.ts
Normal file
441
plugins/importer-curl/src/index.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
import { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
|
||||
import { ControlOperator, parse, ParseEntry } from 'shell-quote';
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
}
|
||||
|
||||
const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii'];
|
||||
const SUPPORTED_FLAGS = [
|
||||
['cookie', 'b'],
|
||||
['d', 'data'], // Add url encoded data
|
||||
['data-ascii'],
|
||||
['data-binary'],
|
||||
['data-raw'],
|
||||
['data-urlencode'],
|
||||
['digest'], // Apply auth as digest
|
||||
['form', 'F'], // Add multipart data
|
||||
['get', 'G'], // Put the post data in the URL
|
||||
['header', 'H'],
|
||||
['request', 'X'], // Request method
|
||||
['url'], // Specify the URL explicitly
|
||||
['url-query'],
|
||||
['user', 'u'], // Authentication
|
||||
DATA_FLAGS,
|
||||
].flatMap((v) => v);
|
||||
|
||||
const BOOLEAN_FLAGS = ['G', 'get', 'digest'];
|
||||
|
||||
type FlagValue = string | boolean;
|
||||
|
||||
type FlagsByName = Record<string, FlagValue[]>;
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'cURL',
|
||||
description: 'Import cURL commands',
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertCurl(args.text) as any;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function convertCurl(rawData: string) {
|
||||
if (!rawData.match(/^\s*curl /)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commands: ParseEntry[][] = [];
|
||||
|
||||
// Replace non-escaped newlines with semicolons to make parsing easier
|
||||
// NOTE: This is really slow in debug build but fast in release mode
|
||||
const normalizedData = rawData.replace(/\ncurl/g, '; curl');
|
||||
|
||||
let currentCommand: ParseEntry[] = [];
|
||||
|
||||
const parsed = parse(normalizedData);
|
||||
|
||||
// Break up `-XPOST` into `-X POST`
|
||||
const normalizedParseEntries = parsed.flatMap((entry) => {
|
||||
if (
|
||||
typeof entry === 'string' &&
|
||||
entry.startsWith('-') &&
|
||||
!entry.startsWith('--') &&
|
||||
entry.length > 2
|
||||
) {
|
||||
return [entry.slice(0, 2), entry.slice(2)];
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
|
||||
for (const parseEntry of normalizedParseEntries) {
|
||||
if (typeof parseEntry === 'string') {
|
||||
if (parseEntry.startsWith('$')) {
|
||||
currentCommand.push(parseEntry.slice(1));
|
||||
} else {
|
||||
currentCommand.push(parseEntry);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('comment' in parseEntry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { op } = parseEntry as { op: 'glob'; pattern: string } | { op: ControlOperator };
|
||||
|
||||
// `;` separates commands
|
||||
if (op === ';') {
|
||||
commands.push(currentCommand);
|
||||
currentCommand = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (op?.startsWith('$')) {
|
||||
// Handle the case where literal like -H $'Header: \'Some Quoted Thing\''
|
||||
const str = op.slice(2, op.length - 1).replace(/\\'/g, '\'');
|
||||
|
||||
currentCommand.push(str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (op === 'glob') {
|
||||
currentCommand.push((parseEntry as { op: 'glob'; pattern: string }).pattern);
|
||||
}
|
||||
}
|
||||
|
||||
commands.push(currentCommand);
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: generateId('workspace'),
|
||||
name: 'Curl Import',
|
||||
};
|
||||
|
||||
const requests: ExportResources['httpRequests'] = commands
|
||||
.filter((command) => command[0] === 'curl')
|
||||
.map((v) => importCommand(v, workspace.id));
|
||||
|
||||
return {
|
||||
resources: {
|
||||
httpRequests: requests,
|
||||
workspaces: [workspace],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
// ~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Collect all the flags //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const flagsByName: FlagsByName = {};
|
||||
const singletons: ParseEntry[] = [];
|
||||
|
||||
// Start at 1 so we can skip the ^curl part
|
||||
for (let i = 1; i < parseEntries.length; i++) {
|
||||
let parseEntry = parseEntries[i];
|
||||
if (typeof parseEntry === 'string') {
|
||||
parseEntry = parseEntry.trim();
|
||||
}
|
||||
|
||||
if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-';
|
||||
let name = parseEntry.replace(/^-{1,2}/, '');
|
||||
|
||||
if (!SUPPORTED_FLAGS.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value;
|
||||
const nextEntry = parseEntries[i + 1];
|
||||
const hasValue = !BOOLEAN_FLAGS.includes(name);
|
||||
if (isSingleDash && name.length > 1) {
|
||||
// Handle squished arguments like -XPOST
|
||||
value = name.slice(1);
|
||||
name = name.slice(0, 1);
|
||||
} else if (typeof nextEntry === 'string' && hasValue && !nextEntry.startsWith('-')) {
|
||||
// Next arg is not a flag, so assign it as the value
|
||||
value = nextEntry;
|
||||
i++; // Skip next one
|
||||
} else {
|
||||
value = true;
|
||||
}
|
||||
|
||||
flagsByName[name] = flagsByName[name] || [];
|
||||
flagsByName[name]!.push(value);
|
||||
} else if (parseEntry) {
|
||||
singletons.push(parseEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~ //
|
||||
// Build the request //
|
||||
// ~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
// Url and Parameters
|
||||
let urlParameters: HttpUrlParameter[];
|
||||
let url: string;
|
||||
|
||||
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
|
||||
const [baseUrl, search] = splitOnce(urlArg, '?');
|
||||
urlParameters =
|
||||
search?.split('&').map((p) => {
|
||||
const v = splitOnce(p, '=');
|
||||
return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true };
|
||||
}) ?? [];
|
||||
|
||||
url = baseUrl ?? urlArg;
|
||||
|
||||
// Query params
|
||||
for (const p of flagsByName['url-query'] ?? []) {
|
||||
if (typeof p !== 'string') {
|
||||
continue;
|
||||
}
|
||||
const [name, value] = p.split('=');
|
||||
urlParameters.push({
|
||||
name: name ?? '',
|
||||
value: value ?? '',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication
|
||||
const [username, password] = getPairValue(flagsByName, '', ['u', 'user']).split(/:(.*)$/);
|
||||
|
||||
const isDigest = getPairValue(flagsByName, false, ['digest']);
|
||||
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
|
||||
const authentication = username
|
||||
? {
|
||||
username: username.trim(),
|
||||
password: (password ?? '').trim(),
|
||||
}
|
||||
: {};
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
...((flagsByName['header'] as string[] | undefined) || []),
|
||||
...((flagsByName['H'] as string[] | undefined) || []),
|
||||
].map((header) => {
|
||||
const [name, value] = header.split(/:(.*)$/);
|
||||
// remove final colon from header name if present
|
||||
if (!value) {
|
||||
return {
|
||||
name: (name ?? '').trim().replace(/;$/, ''),
|
||||
value: '',
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: (name ?? '').trim(),
|
||||
value: value.trim(),
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
|
||||
// Cookies
|
||||
const cookieHeaderValue = [
|
||||
...((flagsByName['cookie'] as string[] | undefined) || []),
|
||||
...((flagsByName['b'] as string[] | undefined) || []),
|
||||
]
|
||||
.map((str) => {
|
||||
const name = str.split('=', 1)[0];
|
||||
const value = str.replace(`${name}=`, '');
|
||||
return `${name}=${value}`;
|
||||
})
|
||||
.join('; ');
|
||||
|
||||
// Convert cookie value to header
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie');
|
||||
|
||||
if (cookieHeaderValue && existingCookieHeader) {
|
||||
// Has existing cookie header, so let's update it
|
||||
existingCookieHeader.value += `; ${cookieHeaderValue}`;
|
||||
} else if (cookieHeaderValue) {
|
||||
// No existing cookie header, so let's make a new one
|
||||
headers.push({
|
||||
name: 'Cookie',
|
||||
value: cookieHeaderValue,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Body (Text or Blob)
|
||||
const dataParameters = pairsToDataParameters(flagsByName);
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0] : null;
|
||||
|
||||
// Body (Multipart Form Data)
|
||||
const formDataParams = [
|
||||
...((flagsByName['form'] as string[] | undefined) || []),
|
||||
...((flagsByName['F'] as string[] | undefined) || []),
|
||||
].map((str) => {
|
||||
const parts = str.split('=');
|
||||
const name = parts[0] ?? '';
|
||||
const value = parts[1] ?? '';
|
||||
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
|
||||
name,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (value.indexOf('@') === 0) {
|
||||
item['file'] = value.slice(1);
|
||||
} else {
|
||||
item['value'] = value;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
// Body
|
||||
let body = {};
|
||||
let bodyType: string | null = null;
|
||||
const bodyAsGET = getPairValue(flagsByName, false, ['G', 'get']);
|
||||
|
||||
if (dataParameters.length > 0 && bodyAsGET) {
|
||||
urlParameters.push(...dataParameters);
|
||||
} else if (
|
||||
dataParameters.length > 0 &&
|
||||
(mimeType == null || mimeType === 'application/x-www-form-urlencoded')
|
||||
) {
|
||||
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
|
||||
body = {
|
||||
form: dataParameters.map((parameter) => ({
|
||||
...parameter,
|
||||
name: decodeURIComponent(parameter.name || ''),
|
||||
value: decodeURIComponent(parameter.value || ''),
|
||||
})),
|
||||
};
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
});
|
||||
} else if (dataParameters.length > 0) {
|
||||
bodyType =
|
||||
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
|
||||
? mimeType
|
||||
: 'other';
|
||||
body = {
|
||||
text: dataParameters
|
||||
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
|
||||
.join('&'),
|
||||
};
|
||||
} else if (formDataParams.length) {
|
||||
bodyType = mimeType ?? 'multipart/form-data';
|
||||
body = {
|
||||
form: formDataParams,
|
||||
};
|
||||
if (mimeType == null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method
|
||||
let method = getPairValue(flagsByName, '', ['X', 'request']).toUpperCase();
|
||||
|
||||
if (method === '' && body) {
|
||||
method = 'text' in body || 'form' in body ? 'POST' : 'GET';
|
||||
}
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
id: generateId('http_request'),
|
||||
model: 'http_request',
|
||||
workspaceId,
|
||||
name: '',
|
||||
urlParameters,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
authentication,
|
||||
authenticationType,
|
||||
body,
|
||||
bodyType,
|
||||
folderId: null,
|
||||
sortPriority: 0,
|
||||
};
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
interface DataParameter {
|
||||
name: string;
|
||||
value: string;
|
||||
contentType?: string;
|
||||
filePath?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
|
||||
let dataParameters: DataParameter[] = [];
|
||||
|
||||
for (const flagName of DATA_FLAGS) {
|
||||
const pairs = keyedPairs[flagName];
|
||||
|
||||
if (!pairs || pairs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const p of pairs) {
|
||||
if (typeof p !== 'string') continue;
|
||||
let params = p.split("&");
|
||||
for (const param of params) {
|
||||
const [name, value] = param.split('=');
|
||||
if (param.startsWith('@')) {
|
||||
// Yaak doesn't support files in url-encoded data, so
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: '',
|
||||
filePath: param.slice(1),
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataParameters;
|
||||
}
|
||||
|
||||
const getPairValue = <T extends string | boolean>(
|
||||
pairsByName: FlagsByName,
|
||||
defaultValue: T,
|
||||
names: string[],
|
||||
) => {
|
||||
for (const name of names) {
|
||||
if (pairsByName[name] && pairsByName[name]!.length) {
|
||||
return pairsByName[name]![0] as T;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function splitOnce(str: string, sep: string): string[] {
|
||||
const index = str.indexOf(sep);
|
||||
if (index > -1) {
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
}
|
||||
return [str];
|
||||
}
|
||||
|
||||
const idCount: Partial<Record<string, number>> = {};
|
||||
|
||||
function generateId(model: string): string {
|
||||
idCount[model] = (idCount[model] ?? -1) + 1;
|
||||
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
|
||||
}
|
||||
400
plugins/importer-curl/tests/index.test.ts
Normal file
400
plugins/importer-curl/tests/index.test.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { HttpRequest, Workspace } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { convertCurl } from '../src';
|
||||
|
||||
describe('importer-curl', () => {
|
||||
test('Imports basic GET', () => {
|
||||
expect(convertCurl('curl https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Explicit URL', () => {
|
||||
expect(convertCurl('curl --url https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Missing URL', () => {
|
||||
expect(convertCurl('curl -X POST')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('URL between', () => {
|
||||
expect(convertCurl('curl -v https://yaak.app -X POST')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Random flags', () => {
|
||||
expect(convertCurl('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports --request method', () => {
|
||||
expect(convertCurl('curl --request POST https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports -XPOST method', () => {
|
||||
expect(convertCurl('curl -XPOST --request POST https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple requests', () => {
|
||||
expect(
|
||||
convertCurl('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({ url: 'https://yaak.app' }),
|
||||
baseRequest({ url: 'example.com' }),
|
||||
baseRequest({ url: 'foo.com' }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports form data', () => {
|
||||
expect(
|
||||
convertCurl('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: [
|
||||
{ enabled: true, name: 'a', value: 'aaa' },
|
||||
{ enabled: true, name: 'b', value: 'bbb' },
|
||||
{ enabled: true, name: 'f', file: 'filepath' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as form url-encoded', () => {
|
||||
expect(convertCurl('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: '', enabled: true },
|
||||
{ name: 'b', value: '', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports combined data params as form url-encoded', () => {
|
||||
expect(convertCurl(`curl -d 'a=aaa&b=bbb&c' https://yaak.app`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa', enabled: true },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: '', enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as text', () => {
|
||||
expect(
|
||||
convertCurl('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }],
|
||||
bodyType: 'text/plain',
|
||||
body: { text: 'a&b&c=ccc' },
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports post data into URL', () => {
|
||||
expect(
|
||||
convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.stripe.com/v1/payment_links',
|
||||
urlParameters: [{
|
||||
enabled: true,
|
||||
name: 'limit',
|
||||
value: '3',
|
||||
}],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multi-line JSON', () => {
|
||||
expect(
|
||||
convertCurl(`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
body: { text: '{\n "foo":"bar"\n}' },
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple headers', () => {
|
||||
expect(
|
||||
convertCurl('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [
|
||||
{ name: 'Name', value: '', enabled: true },
|
||||
{ name: 'Foo', value: 'bar', enabled: true },
|
||||
{ name: 'AAA', value: 'bbb', enabled: true },
|
||||
{ name: '', value: 'ccc', enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports basic auth', () => {
|
||||
expect(convertCurl('curl --user user:pass https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports digest auth', () => {
|
||||
expect(convertCurl('curl --digest --user user:pass https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'digest',
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports cookie as header', () => {
|
||||
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params', () => {
|
||||
expect(convertCurl('curl "https://yaak.app" --url-query foo=bar --url-query baz=qux')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'qux', enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params from the URL', () => {
|
||||
expect(convertCurl('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'a a', enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const idCount: Partial<Record<string, number>> = {};
|
||||
|
||||
function baseRequest(mergeWith: Partial<HttpRequest>) {
|
||||
idCount.http_request = (idCount.http_request ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`,
|
||||
model: 'http_request',
|
||||
authentication: {},
|
||||
authenticationType: null,
|
||||
body: {},
|
||||
bodyType: null,
|
||||
folderId: null,
|
||||
headers: [],
|
||||
method: 'GET',
|
||||
name: '',
|
||||
sortPriority: 0,
|
||||
url: '',
|
||||
urlParameters: [],
|
||||
workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
...mergeWith,
|
||||
};
|
||||
}
|
||||
|
||||
function baseWorkspace(mergeWith: Partial<Workspace> = {}) {
|
||||
idCount.workspace = (idCount.workspace ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
model: 'workspace',
|
||||
name: 'Curl Import',
|
||||
...mergeWith,
|
||||
};
|
||||
}
|
||||
34
plugins/importer-insomnia/src/common.ts
Normal file
34
plugins/importer-insomnia/src/common.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
export function convertSyntax(variable: string): string {
|
||||
if (!isJSString(variable)) return variable;
|
||||
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
|
||||
}
|
||||
|
||||
export function isJSObject(obj: any) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
}
|
||||
|
||||
export function isJSString(obj: any) {
|
||||
return Object.prototype.toString.call(obj) === '[object String]';
|
||||
}
|
||||
|
||||
export function convertId(id: string): string {
|
||||
if (id.startsWith('GENERATE_ID::')) {
|
||||
return id;
|
||||
}
|
||||
return `GENERATE_ID::${id}`;
|
||||
}
|
||||
|
||||
export function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs) as T;
|
||||
} else if (typeof obj === 'object' && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
|
||||
) as T;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
35
plugins/importer-insomnia/src/index.ts
Normal file
35
plugins/importer-insomnia/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import YAML from 'yaml';
|
||||
import { deleteUndefinedAttrs, isJSObject } from './common';
|
||||
import { convertInsomniaV4 } from './v4';
|
||||
import { convertInsomniaV5 } from './v5';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Insomnia',
|
||||
description: 'Import Insomnia workspaces',
|
||||
async onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertInsomnia(args.text);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function convertInsomnia(contents: string) {
|
||||
let parsed: any;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
try {
|
||||
parsed = parsed ?? YAML.parse(contents);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (!isJSObject(parsed)) return null;
|
||||
|
||||
const result = convertInsomniaV5(parsed) ?? convertInsomniaV4(parsed);
|
||||
|
||||
return deleteUndefinedAttrs(result);
|
||||
}
|
||||
206
plugins/importer-insomnia/src/v4.ts
Normal file
206
plugins/importer-insomnia/src/v4.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { PartialImportResources } from '@yaakapp/api';
|
||||
import { convertId, convertSyntax, isJSObject } from './common';
|
||||
|
||||
export function convertInsomniaV4(parsed: Record<string, any>) {
|
||||
if (!Array.isArray(parsed.resources)) return null;
|
||||
|
||||
const resources: PartialImportResources = {
|
||||
environments: [],
|
||||
folders: [],
|
||||
grpcRequests: [],
|
||||
httpRequests: [],
|
||||
websocketRequests: [],
|
||||
workspaces: [],
|
||||
};
|
||||
|
||||
// Import workspaces
|
||||
const workspacesToImport = parsed.resources.filter(r => isJSObject(r) && r._type === 'workspace');
|
||||
for (const w of workspacesToImport) {
|
||||
resources.workspaces.push({
|
||||
id: convertId(w._id),
|
||||
createdAt: w.created ? new Date(w.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: w.updated ? new Date(w.updated).toISOString().replace('Z', '') : undefined,
|
||||
model: 'workspace',
|
||||
name: w.name,
|
||||
description: w.description || undefined,
|
||||
});
|
||||
const environmentsToImport = parsed.resources.filter(
|
||||
(r: any) => isJSObject(r) && r._type === 'environment',
|
||||
);
|
||||
resources.environments.push(
|
||||
...environmentsToImport.map((r: any) => importEnvironment(r, w._id)),
|
||||
);
|
||||
|
||||
const nextFolder = (parentId: string) => {
|
||||
const children = parsed.resources.filter((r: any) => r.parentId === parentId);
|
||||
for (const child of children) {
|
||||
if (!isJSObject(child)) continue;
|
||||
|
||||
if (child._type === 'request_group') {
|
||||
resources.folders.push(importFolder(child, w._id));
|
||||
nextFolder(child._id);
|
||||
} else if (child._type === 'request') {
|
||||
resources.httpRequests.push(
|
||||
importHttpRequest(child, w._id),
|
||||
);
|
||||
} else if (child._type === 'grpc_request') {
|
||||
resources.grpcRequests.push(
|
||||
importGrpcRequest(child, w._id),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Import folders
|
||||
nextFolder(w._id);
|
||||
}
|
||||
|
||||
// Filter out any `null` values
|
||||
resources.httpRequests = resources.httpRequests.filter(Boolean);
|
||||
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
|
||||
return { resources };
|
||||
}
|
||||
|
||||
function importHttpRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
): PartialImportResources['httpRequests'][0] {
|
||||
let bodyType: string | null = null;
|
||||
let body = {};
|
||||
if (r.body.mimeType === 'application/octet-stream') {
|
||||
bodyType = 'binary';
|
||||
body = { filePath: r.body.fileName ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
|
||||
bodyType = 'application/x-www-form-urlencoded';
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'multipart/form-data') {
|
||||
bodyType = 'multipart/form-data';
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
file: p.fileName ?? null,
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'application/graphql') {
|
||||
bodyType = 'graphql';
|
||||
body = { text: convertSyntax(r.body.text ?? '') };
|
||||
} else if (r.body?.mimeType === 'application/json') {
|
||||
bodyType = 'application/json';
|
||||
body = { text: convertSyntax(r.body.text ?? '') };
|
||||
}
|
||||
|
||||
let authenticationType: string | null = null;
|
||||
let authentication = {};
|
||||
if (r.authentication.type === 'bearer') {
|
||||
authenticationType = 'bearer';
|
||||
authentication = {
|
||||
token: convertSyntax(r.authentication.token),
|
||||
};
|
||||
} else if (r.authentication.type === 'basic') {
|
||||
authenticationType = 'basic';
|
||||
authentication = {
|
||||
username: convertSyntax(r.authentication.username),
|
||||
password: convertSyntax(r.authentication.password),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: 'http_request',
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: convertSyntax(r.url),
|
||||
body,
|
||||
bodyType,
|
||||
authentication,
|
||||
authenticationType,
|
||||
method: r.method,
|
||||
headers: (r.headers ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
};
|
||||
}
|
||||
|
||||
function importGrpcRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
): PartialImportResources['grpcRequests'][0] {
|
||||
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
|
||||
return {
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: 'grpc_request',
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? '',
|
||||
metadata: (r.metadata ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
};
|
||||
}
|
||||
|
||||
function importFolder(f: any, workspaceId: string): PartialImportResources['folders'][0] {
|
||||
return {
|
||||
id: convertId(f._id),
|
||||
createdAt: f.created ? new Date(f.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: f.modified ? new Date(f.modified).toISOString().replace('Z', '') : undefined,
|
||||
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || undefined,
|
||||
model: 'folder',
|
||||
name: f.name,
|
||||
};
|
||||
}
|
||||
|
||||
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
|
||||
return {
|
||||
id: convertId(e._id),
|
||||
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
// @ts-ignore
|
||||
sortPriority: e.metaSortKey, // Will be added to Yaak later
|
||||
base: isParent ?? e.parentId === workspaceId,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
265
plugins/importer-insomnia/src/v5.ts
Normal file
265
plugins/importer-insomnia/src/v5.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { PartialImportResources } from '@yaakapp/api';
|
||||
import { convertId, convertSyntax, isJSObject } from './common';
|
||||
|
||||
export function convertInsomniaV5(parsed: Record<string, any>) {
|
||||
if (!Array.isArray(parsed.collection)) return null;
|
||||
|
||||
const resources: PartialImportResources = {
|
||||
environments: [],
|
||||
folders: [],
|
||||
grpcRequests: [],
|
||||
httpRequests: [],
|
||||
websocketRequests: [],
|
||||
workspaces: [],
|
||||
};
|
||||
|
||||
// Import workspaces
|
||||
const meta: Record<string, any> = parsed.meta ?? {};
|
||||
resources.workspaces.push({
|
||||
id: convertId(meta.id ?? 'collection'),
|
||||
createdAt: meta.created ? new Date(meta.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace('Z', '') : undefined,
|
||||
model: 'workspace',
|
||||
name: parsed.name,
|
||||
description: meta.description || undefined,
|
||||
});
|
||||
resources.environments.push(
|
||||
importEnvironment(parsed.environments, meta.id, true),
|
||||
...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)),
|
||||
);
|
||||
|
||||
const nextFolder = (children: any[], parentId: string) => {
|
||||
for (const child of children ?? []) {
|
||||
if (!isJSObject(child)) continue;
|
||||
|
||||
if (Array.isArray(child.children)) {
|
||||
resources.folders.push(importFolder(child, meta.id, parentId));
|
||||
nextFolder(child.children, child.meta.id);
|
||||
} else if (child.method) {
|
||||
resources.httpRequests.push(
|
||||
importHttpRequest(child, meta.id, parentId),
|
||||
);
|
||||
} else if (child.protoFileId) {
|
||||
resources.grpcRequests.push(
|
||||
importGrpcRequest(child, meta.id, parentId),
|
||||
);
|
||||
} else if (child.url) {
|
||||
resources.websocketRequests.push(
|
||||
importWebsocketRequest(child, meta.id, parentId),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Import folders
|
||||
nextFolder(parsed.collection ?? [], meta.id);
|
||||
|
||||
// Filter out any `null` values
|
||||
resources.httpRequests = resources.httpRequests.filter(Boolean);
|
||||
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
|
||||
return { resources };
|
||||
}
|
||||
|
||||
function importHttpRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['httpRequests'][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
|
||||
let bodyType: string | null = null;
|
||||
let body = {};
|
||||
if (r.body?.mimeType === 'application/octet-stream') {
|
||||
bodyType = 'binary';
|
||||
body = { filePath: r.body.fileName ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
|
||||
bodyType = 'application/x-www-form-urlencoded';
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'multipart/form-data') {
|
||||
bodyType = 'multipart/form-data';
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
file: p.fileName ?? null,
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'application/graphql') {
|
||||
bodyType = 'graphql';
|
||||
body = { text: convertSyntax(r.body.text ?? '') };
|
||||
} else if (r.body?.mimeType === 'application/json') {
|
||||
bodyType = 'application/json';
|
||||
body = { text: convertSyntax(r.body.text ?? '') };
|
||||
}
|
||||
|
||||
return {
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
model: 'http_request',
|
||||
name: r.name,
|
||||
description: r.meta?.description || undefined,
|
||||
url: convertSyntax(r.url),
|
||||
body,
|
||||
bodyType,
|
||||
method: r.method,
|
||||
...importHeaders(r),
|
||||
...importAuthentication(r),
|
||||
};
|
||||
}
|
||||
|
||||
function importGrpcRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['grpcRequests'][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
|
||||
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
|
||||
return {
|
||||
model: 'grpc_request',
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? '',
|
||||
metadata: (r.metadata ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
};
|
||||
}
|
||||
|
||||
function importWebsocketRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['websocketRequests'][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
|
||||
return {
|
||||
model: 'websocket_request',
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: convertSyntax(r.url),
|
||||
message: r.body?.text ?? '',
|
||||
...importHeaders(r),
|
||||
...importAuthentication(r),
|
||||
};
|
||||
}
|
||||
|
||||
function importHeaders(r: any) {
|
||||
const headers = (r.headers ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== '');
|
||||
return { headers } as const;
|
||||
}
|
||||
|
||||
function importAuthentication(r: any) {
|
||||
let authenticationType: string | null = null;
|
||||
let authentication = {};
|
||||
if (r.authentication?.type === 'bearer') {
|
||||
authenticationType = 'bearer';
|
||||
authentication = {
|
||||
token: convertSyntax(r.authentication.token),
|
||||
};
|
||||
} else if (r.authentication?.type === 'basic') {
|
||||
authenticationType = 'basic';
|
||||
authentication = {
|
||||
username: convertSyntax(r.authentication.username),
|
||||
password: convertSyntax(r.authentication.password),
|
||||
};
|
||||
}
|
||||
|
||||
return { authenticationType, authentication } as const;
|
||||
}
|
||||
|
||||
function importFolder(f: any, workspaceId: string, parentId: string): PartialImportResources['folders'][0] {
|
||||
const id = f.meta?.id ?? f._id;
|
||||
const created = f.meta?.created ?? f.created;
|
||||
const updated = f.meta?.modified ?? f.updated;
|
||||
const sortKey = f.meta?.sortKey ?? f.sortKey;
|
||||
|
||||
return {
|
||||
model: 'folder',
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || undefined,
|
||||
name: f.name,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
|
||||
const id = e.meta?.id ?? e._id;
|
||||
const created = e.meta?.created ?? e.created;
|
||||
const updated = e.meta?.modified ?? e.updated;
|
||||
const sortKey = e.meta?.sortKey ?? e.sortKey;
|
||||
|
||||
return {
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
public: !e.isPrivate,
|
||||
// @ts-ignore
|
||||
sortPriority: sortKey, // Will be added to Yaak later
|
||||
base: isParent ?? e.parentId === workspaceId,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
187
plugins/importer-insomnia/tests/fixtures/basic.input.json
vendored
Normal file
187
plugins/importer-insomnia/tests/fixtures/basic.input.json
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"_type": "export",
|
||||
"__export_format": 4,
|
||||
"__export_date": "2025-01-13T15:19:18.330Z",
|
||||
"__export_source": "insomnia.desktop.app:v10.3.0",
|
||||
"resources": [
|
||||
{
|
||||
"_id": "req_84cd9ae4bd034dd8bb730e856a665cbb",
|
||||
"parentId": "fld_859d1df78261463480b6a3a1419517e3",
|
||||
"modified": 1736781473176,
|
||||
"created": 1736781406672,
|
||||
"url": "{{ _.BASE_URL }}/foo/:id",
|
||||
"name": "New Request",
|
||||
"description": "My description of the request",
|
||||
"method": "GET",
|
||||
"body": {
|
||||
"mimeType": "multipart/form-data",
|
||||
"params": [
|
||||
{
|
||||
"id": "pair_7c86036ae8ef499dbbc0b43d0800c5a3",
|
||||
"name": "form",
|
||||
"value": "data",
|
||||
"description": "",
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"id": "pair_b22f6ff611cd4250a6e405ca7b713d09",
|
||||
"name": "query",
|
||||
"value": "qqq",
|
||||
"description": "",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/form-data",
|
||||
"id": "pair_4af845963bd14256b98716617971eecd"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/10.3.0",
|
||||
"id": "pair_535ffd00ce48462cb1b7258832ade65a"
|
||||
},
|
||||
{
|
||||
"id": "pair_ab4b870278e943cba6babf5a73e213e3",
|
||||
"name": "X-Header",
|
||||
"value": "xxxx",
|
||||
"description": "",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"authentication": {
|
||||
"type": "basic",
|
||||
"useISO88591": false,
|
||||
"disabled": false,
|
||||
"username": "user",
|
||||
"password": "pass"
|
||||
},
|
||||
"metaSortKey": -1736781406672,
|
||||
"isPrivate": false,
|
||||
"pathParameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "iii"
|
||||
}
|
||||
],
|
||||
"settingStoreCookies": true,
|
||||
"settingSendCookies": true,
|
||||
"settingDisableRenderRequestBody": false,
|
||||
"settingEncodeUrl": true,
|
||||
"settingRebuildPath": true,
|
||||
"settingFollowRedirects": "global",
|
||||
"_type": "request"
|
||||
},
|
||||
{
|
||||
"_id": "fld_859d1df78261463480b6a3a1419517e3",
|
||||
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
|
||||
"modified": 1736781404718,
|
||||
"created": 1736781404718,
|
||||
"name": "Top Level",
|
||||
"description": "",
|
||||
"environment": {},
|
||||
"environmentPropertyOrder": null,
|
||||
"metaSortKey": -1736781404718,
|
||||
"environmentType": "kv",
|
||||
"_type": "request_group"
|
||||
},
|
||||
{
|
||||
"_id": "wrk_d4d92f7c0ee947b89159243506687019",
|
||||
"parentId": null,
|
||||
"modified": 1736781343765,
|
||||
"created": 1736781343765,
|
||||
"name": "Dummy",
|
||||
"description": "",
|
||||
"scope": "collection",
|
||||
"_type": "workspace"
|
||||
},
|
||||
{
|
||||
"_id": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
|
||||
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
|
||||
"modified": 1736781355209,
|
||||
"created": 1736781343767,
|
||||
"name": "Base Environment",
|
||||
"data": {
|
||||
"BASE_VAR": "hello"
|
||||
},
|
||||
"dataPropertyOrder": null,
|
||||
"color": null,
|
||||
"isPrivate": false,
|
||||
"metaSortKey": 1736781343767,
|
||||
"environmentType": "kv",
|
||||
"kvPairData": [
|
||||
{
|
||||
"id": "envPair_61c1be66d42241b5a28306d2cd92d3e3",
|
||||
"name": "BASE_VAR",
|
||||
"value": "hello",
|
||||
"type": "str",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_type": "environment"
|
||||
},
|
||||
{
|
||||
"_id": "jar_16c0dec5b77c414ae0e419b8f10c3701300c5900",
|
||||
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
|
||||
"modified": 1736781343768,
|
||||
"created": 1736781343768,
|
||||
"name": "Default Jar",
|
||||
"cookies": [],
|
||||
"_type": "cookie_jar"
|
||||
},
|
||||
{
|
||||
"_id": "env_799ae3d723ef44af91b4817e5d057e6d",
|
||||
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
|
||||
"modified": 1736781394705,
|
||||
"created": 1736781358515,
|
||||
"name": "Production",
|
||||
"data": {
|
||||
"BASE_URL": "https://api.yaak.app"
|
||||
},
|
||||
"dataPropertyOrder": null,
|
||||
"color": "#f22c2c",
|
||||
"isPrivate": false,
|
||||
"metaSortKey": 1736781358515,
|
||||
"environmentType": "kv",
|
||||
"kvPairData": [
|
||||
{
|
||||
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.yaak.app",
|
||||
"type": "str",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_type": "environment"
|
||||
},
|
||||
{
|
||||
"_id": "env_030fbfdbb274426ebd78e2e6518f8553",
|
||||
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
|
||||
"modified": 1736781391078,
|
||||
"created": 1736781374707,
|
||||
"name": "Staging",
|
||||
"data": {
|
||||
"BASE_URL": "https://api.staging.yaak.app"
|
||||
},
|
||||
"dataPropertyOrder": null,
|
||||
"color": "#206fac",
|
||||
"isPrivate": false,
|
||||
"metaSortKey": 1736781358565,
|
||||
"environmentType": "kv",
|
||||
"kvPairData": [
|
||||
{
|
||||
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.staging.yaak.app",
|
||||
"type": "str",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_type": "environment"
|
||||
}
|
||||
]
|
||||
}
|
||||
126
plugins/importer-insomnia/tests/fixtures/basic.output.json
vendored
Normal file
126
plugins/importer-insomnia/tests/fixtures/basic.output.json
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"resources": {
|
||||
"environments": [
|
||||
{
|
||||
"createdAt": "2025-01-13T15:15:43.767",
|
||||
"updatedAt": "2025-01-13T15:15:55.209",
|
||||
"sortPriority": 1736781343767,
|
||||
"base": true,
|
||||
"id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
|
||||
"model": "environment",
|
||||
"name": "Base Environment",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_VAR",
|
||||
"value": "hello"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
|
||||
},
|
||||
{
|
||||
"createdAt": "2025-01-13T15:15:58.515",
|
||||
"updatedAt": "2025-01-13T15:16:34.705",
|
||||
"sortPriority": 1736781358515,
|
||||
"base": false,
|
||||
"id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d",
|
||||
"model": "environment",
|
||||
"name": "Production",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.yaak.app"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
|
||||
},
|
||||
{
|
||||
"createdAt": "2025-01-13T15:16:14.707",
|
||||
"updatedAt": "2025-01-13T15:16:31.078",
|
||||
"sortPriority": 1736781358565,
|
||||
"base": false,
|
||||
"id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553",
|
||||
"model": "environment",
|
||||
"name": "Staging",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.staging.yaak.app"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
|
||||
}
|
||||
],
|
||||
"folders": [
|
||||
{
|
||||
"createdAt": "2025-01-13T15:16:44.718",
|
||||
"updatedAt": "2025-01-13T15:16:44.718",
|
||||
"folderId": null,
|
||||
"id": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
|
||||
"model": "folder",
|
||||
"name": "Top Level",
|
||||
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
|
||||
}
|
||||
],
|
||||
"grpcRequests": [],
|
||||
"httpRequests": [
|
||||
{
|
||||
"authentication": {
|
||||
"password": "pass",
|
||||
"username": "user"
|
||||
},
|
||||
"authenticationType": "basic",
|
||||
"body": {
|
||||
"form": [
|
||||
{
|
||||
"enabled": true,
|
||||
"file": null,
|
||||
"name": "form",
|
||||
"value": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bodyType": "multipart/form-data",
|
||||
"createdAt": "2025-01-13T15:16:46.672",
|
||||
"sortPriority": -1736781406672,
|
||||
"updatedAt": "2025-01-13T15:17:53.176",
|
||||
"description": "My description of the request",
|
||||
"folderId": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
|
||||
"headers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/form-data"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/10.3.0"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "X-Header",
|
||||
"value": "xxxx"
|
||||
}
|
||||
],
|
||||
"id": "GENERATE_ID::req_84cd9ae4bd034dd8bb730e856a665cbb",
|
||||
"method": "GET",
|
||||
"model": "http_request",
|
||||
"name": "New Request",
|
||||
"url": "${[BASE_URL ]}/foo/:id",
|
||||
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
|
||||
}
|
||||
],
|
||||
"websocketRequests": [],
|
||||
"workspaces": [
|
||||
{
|
||||
"createdAt": "2025-01-13T15:15:43.765",
|
||||
"id": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019",
|
||||
"model": "workspace",
|
||||
"name": "Dummy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
72
plugins/importer-insomnia/tests/fixtures/version-5-minimal.input.yaml
vendored
Normal file
72
plugins/importer-insomnia/tests/fixtures/version-5-minimal.input.yaml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
type: collection.insomnia.rest/5.0
|
||||
name: Debugging
|
||||
meta:
|
||||
id: wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c
|
||||
created: 1747197924902
|
||||
modified: 1747197924902
|
||||
collection:
|
||||
- name: My Folder
|
||||
meta:
|
||||
id: fld_296933ea4ea84783a775d199997e9be7
|
||||
created: 1747414092298
|
||||
modified: 1747414142427
|
||||
sortKey: -1747414092298
|
||||
children:
|
||||
- url: https://httpbin.org/post
|
||||
name: New Request
|
||||
meta:
|
||||
id: req_9a80320365ac4509ade406359dbc6a71
|
||||
created: 1747197928502
|
||||
modified: 1747414129313
|
||||
isPrivate: false
|
||||
sortKey: -1747414129276
|
||||
method: GET
|
||||
headers:
|
||||
- name: User-Agent
|
||||
value: insomnia/11.1.0
|
||||
id: pair_6ae87d1620a9494f8e5b29cd9f92d087
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
rebuildPath: true
|
||||
headers:
|
||||
- id: pair_f2b330e3914f4c11b209318aef94325c
|
||||
name: foo
|
||||
value: bar
|
||||
disabled: false
|
||||
- name: New Request
|
||||
meta:
|
||||
id: req_e3f8cdbd58784a539dd4c1e127d73451
|
||||
created: 1747414160497
|
||||
modified: 1747414160497
|
||||
isPrivate: false
|
||||
sortKey: -1747414160498
|
||||
method: GET
|
||||
headers:
|
||||
- name: User-Agent
|
||||
value: insomnia/11.1.0
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
rebuildPath: true
|
||||
cookieJar:
|
||||
name: Default Jar
|
||||
meta:
|
||||
id: jar_e46dc73e8ccda30ca132153e8f11183bd08119ce
|
||||
created: 1747197924904
|
||||
modified: 1747197924904
|
||||
environments:
|
||||
name: Base Environment
|
||||
meta:
|
||||
id: env_e46dc73e8ccda30ca132153e8f11183bd08119ce
|
||||
created: 1747197924903
|
||||
modified: 1747197924903
|
||||
isPrivate: false
|
||||
87
plugins/importer-insomnia/tests/fixtures/version-5-minimal.output.json
vendored
Normal file
87
plugins/importer-insomnia/tests/fixtures/version-5-minimal.output.json
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"resources": {
|
||||
"environments": [
|
||||
{
|
||||
"base": true,
|
||||
"createdAt": "2025-05-14T04:45:24.903",
|
||||
"id": "GENERATE_ID::env_e46dc73e8ccda30ca132153e8f11183bd08119ce",
|
||||
"model": "environment",
|
||||
"name": "Base Environment",
|
||||
"public": true,
|
||||
"updatedAt": "2025-05-14T04:45:24.903",
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
|
||||
}
|
||||
],
|
||||
"folders": [
|
||||
{
|
||||
"createdAt": "2025-05-16T16:48:12.298",
|
||||
"folderId": null,
|
||||
"id": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
|
||||
"model": "folder",
|
||||
"name": "My Folder",
|
||||
"sortPriority": -1747414092298,
|
||||
"updatedAt": "2025-05-16T16:49:02.427",
|
||||
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
|
||||
}
|
||||
],
|
||||
"grpcRequests": [],
|
||||
"httpRequests": [
|
||||
{
|
||||
"authentication": {},
|
||||
"authenticationType": null,
|
||||
"body": {},
|
||||
"bodyType": null,
|
||||
"createdAt": "2025-05-14T04:45:28.502",
|
||||
"folderId": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
|
||||
"headers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/11.1.0"
|
||||
}
|
||||
],
|
||||
"id": "GENERATE_ID::req_9a80320365ac4509ade406359dbc6a71",
|
||||
"method": "GET",
|
||||
"model": "http_request",
|
||||
"name": "New Request",
|
||||
"sortPriority": -1747414129276,
|
||||
"updatedAt": "2025-05-16T16:48:49.313",
|
||||
"url": "https://httpbin.org/post",
|
||||
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
|
||||
},
|
||||
{
|
||||
"authentication": {},
|
||||
"authenticationType": null,
|
||||
"body": {},
|
||||
"bodyType": null,
|
||||
"createdAt": "2025-05-16T16:49:20.497",
|
||||
"folderId": null,
|
||||
"headers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/11.1.0"
|
||||
}
|
||||
],
|
||||
"id": "GENERATE_ID::req_e3f8cdbd58784a539dd4c1e127d73451",
|
||||
"method": "GET",
|
||||
"model": "http_request",
|
||||
"name": "New Request",
|
||||
"sortPriority": -1747414160498,
|
||||
"updatedAt": "2025-05-16T16:49:20.497",
|
||||
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
|
||||
}
|
||||
],
|
||||
"websocketRequests": [],
|
||||
"workspaces": [
|
||||
{
|
||||
"createdAt": "2025-05-14T04:45:24.902",
|
||||
"id": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c",
|
||||
"model": "workspace",
|
||||
"name": "Debugging",
|
||||
"updatedAt": "2025-05-14T04:45:24.902"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
142
plugins/importer-insomnia/tests/fixtures/version-5.input.yaml
vendored
Normal file
142
plugins/importer-insomnia/tests/fixtures/version-5.input.yaml
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
type: collection.insomnia.rest/5.0
|
||||
name: Dummy
|
||||
meta:
|
||||
id: wrk_c1eacfa750a04f3ea9985ef28043fa53
|
||||
created: 1746799305927
|
||||
modified: 1746843054272
|
||||
description: This is the description
|
||||
collection:
|
||||
- name: Top Level
|
||||
meta:
|
||||
id: fld_42eb2e2bb22b4cedacbd3d057634e80c
|
||||
created: 1736781404718
|
||||
modified: 1736781404718
|
||||
sortKey: -1736781404718
|
||||
children:
|
||||
- url: "{{ _.BASE_URL }}/foo/:id"
|
||||
name: New Request
|
||||
meta:
|
||||
id: req_d72fff2a6b104b91a2ebe9de9edd2785
|
||||
created: 1736781406672
|
||||
modified: 1736781473176
|
||||
isPrivate: false
|
||||
description: My description of the request
|
||||
sortKey: -1736781406672
|
||||
method: GET
|
||||
body:
|
||||
mimeType: multipart/form-data
|
||||
params:
|
||||
- id: pair_7c86036ae8ef499dbbc0b43d0800c5a3
|
||||
name: form
|
||||
value: data
|
||||
disabled: false
|
||||
parameters:
|
||||
- id: pair_b22f6ff611cd4250a6e405ca7b713d09
|
||||
name: query
|
||||
value: qqq
|
||||
disabled: false
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: multipart/form-data
|
||||
id: pair_4af845963bd14256b98716617971eecd
|
||||
- name: User-Agent
|
||||
value: insomnia/10.3.0
|
||||
id: pair_535ffd00ce48462cb1b7258832ade65a
|
||||
- id: pair_ab4b870278e943cba6babf5a73e213e3
|
||||
name: X-Header
|
||||
value: xxxx
|
||||
disabled: false
|
||||
authentication:
|
||||
type: basic
|
||||
useISO88591: false
|
||||
disabled: false
|
||||
username: user
|
||||
password: pass
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
rebuildPath: true
|
||||
pathParameters:
|
||||
- name: id
|
||||
value: iii
|
||||
- url: grpcb.in:9000
|
||||
name: New Request
|
||||
meta:
|
||||
id: greq_06d659324df94504a4d64632be7106b3
|
||||
created: 1746799344864
|
||||
modified: 1746799544082
|
||||
isPrivate: false
|
||||
sortKey: -1746799344864
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"greeting": "Greg"
|
||||
}
|
||||
protoFileId: pf_9d45b0dfaccc4bcc9d930746716786c5
|
||||
protoMethodName: /hello.HelloService/SayHello
|
||||
reflectionApi:
|
||||
enabled: false
|
||||
url: https://buf.build
|
||||
module: buf.build/connectrpc/eliza
|
||||
- url: wss://echo.websocket.org
|
||||
name: New WebSocket Request
|
||||
meta:
|
||||
id: ws-req_5d1a4c7c79494743962e5176f6add270
|
||||
created: 1746799553909
|
||||
modified: 1746887120958
|
||||
sortKey: -1746799553909
|
||||
settings:
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
authentication:
|
||||
type: basic
|
||||
useISO88591: false
|
||||
disabled: false
|
||||
username: user
|
||||
password: password
|
||||
headers:
|
||||
- name: User-Agent
|
||||
value: insomnia/11.1.0
|
||||
cookieJar:
|
||||
name: Default Jar
|
||||
meta:
|
||||
id: jar_663d5741b072441aa2709a6113371510
|
||||
created: 1736781343768
|
||||
modified: 1736781343768
|
||||
environments:
|
||||
name: Base Environment
|
||||
meta:
|
||||
id: env_20945044d3c8497ca8b717bef750987e
|
||||
created: 1736781343767
|
||||
modified: 1736781355209
|
||||
isPrivate: false
|
||||
data:
|
||||
BASE_VAR: hello
|
||||
subEnvironments:
|
||||
- name: Production
|
||||
meta:
|
||||
id: env_6f7728bb7fc04d558d668e954d756ea2
|
||||
created: 1736781358515
|
||||
modified: 1736781394705
|
||||
isPrivate: false
|
||||
sortKey: 1736781358515
|
||||
data:
|
||||
BASE_URL: https://api.yaak.app
|
||||
color: "#f22c2c"
|
||||
- name: Staging
|
||||
meta:
|
||||
id: env_976a8b6eb5d44fb6a20150f65c32d243
|
||||
created: 1736781374707
|
||||
modified: 1736781391078
|
||||
isPrivate: false
|
||||
sortKey: 1736781358565
|
||||
data:
|
||||
BASE_URL: https://api.staging.yaak.app
|
||||
color: "#206fac"
|
||||
172
plugins/importer-insomnia/tests/fixtures/version-5.output.json
vendored
Normal file
172
plugins/importer-insomnia/tests/fixtures/version-5.output.json
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"resources": {
|
||||
"environments": [
|
||||
{
|
||||
"createdAt": "2025-01-13T15:15:43.767",
|
||||
"updatedAt": "2025-01-13T15:15:55.209",
|
||||
"base": true,
|
||||
"public": true,
|
||||
"id": "GENERATE_ID::env_20945044d3c8497ca8b717bef750987e",
|
||||
"model": "environment",
|
||||
"name": "Base Environment",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_VAR",
|
||||
"value": "hello"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
},
|
||||
{
|
||||
"createdAt": "2025-01-13T15:15:58.515",
|
||||
"updatedAt": "2025-01-13T15:16:34.705",
|
||||
"base": false,
|
||||
"public": true,
|
||||
"id": "GENERATE_ID::env_6f7728bb7fc04d558d668e954d756ea2",
|
||||
"model": "environment",
|
||||
"name": "Production",
|
||||
"sortPriority": 1736781358515,
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.yaak.app"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
},
|
||||
{
|
||||
"createdAt": "2025-01-13T15:16:14.707",
|
||||
"updatedAt": "2025-01-13T15:16:31.078",
|
||||
"base": false,
|
||||
"public": true,
|
||||
"id": "GENERATE_ID::env_976a8b6eb5d44fb6a20150f65c32d243",
|
||||
"model": "environment",
|
||||
"name": "Staging",
|
||||
"sortPriority": 1736781358565,
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "BASE_URL",
|
||||
"value": "https://api.staging.yaak.app"
|
||||
}
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
}
|
||||
],
|
||||
"folders": [
|
||||
{
|
||||
"createdAt": "2025-01-13T15:16:44.718",
|
||||
"updatedAt": "2025-01-13T15:16:44.718",
|
||||
"folderId": null,
|
||||
"id": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c",
|
||||
"model": "folder",
|
||||
"name": "Top Level",
|
||||
"sortPriority": -1736781404718,
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
}
|
||||
],
|
||||
"grpcRequests": [
|
||||
{
|
||||
"model": "grpc_request",
|
||||
"createdAt": "2025-05-09T14:02:24.864",
|
||||
"folderId": null,
|
||||
"id": "GENERATE_ID::greq_06d659324df94504a4d64632be7106b3",
|
||||
"message": "{\n\t\"greeting\": \"Greg\"\n}",
|
||||
"metadata": [],
|
||||
"method": "SayHello",
|
||||
"name": "New Request",
|
||||
"service": "hello.HelloService",
|
||||
"sortPriority": -1746799344864,
|
||||
"updatedAt": "2025-05-09T14:05:44.082",
|
||||
"url": "grpcb.in:9000",
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
}
|
||||
],
|
||||
"httpRequests": [
|
||||
{
|
||||
"authentication": {
|
||||
"password": "pass",
|
||||
"username": "user"
|
||||
},
|
||||
"authenticationType": "basic",
|
||||
"body": {
|
||||
"form": [
|
||||
{
|
||||
"enabled": true,
|
||||
"file": null,
|
||||
"name": "form",
|
||||
"value": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bodyType": "multipart/form-data",
|
||||
"createdAt": "2025-01-13T15:16:46.672",
|
||||
"updatedAt": "2025-01-13T15:17:53.176",
|
||||
"description": "My description of the request",
|
||||
"folderId": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c",
|
||||
"headers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/form-data"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/10.3.0"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "X-Header",
|
||||
"value": "xxxx"
|
||||
}
|
||||
],
|
||||
"id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785",
|
||||
"method": "GET",
|
||||
"model": "http_request",
|
||||
"name": "New Request",
|
||||
"sortPriority": -1736781406672,
|
||||
"url": "${[BASE_URL ]}/foo/:id",
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
}
|
||||
],
|
||||
"websocketRequests": [
|
||||
{
|
||||
"id": "GENERATE_ID::ws-req_5d1a4c7c79494743962e5176f6add270",
|
||||
"createdAt": "2025-05-09T14:05:53.909",
|
||||
"updatedAt": "2025-05-10T14:25:20.958",
|
||||
"message": "",
|
||||
"model": "websocket_request",
|
||||
"name": "New WebSocket Request",
|
||||
"sortPriority": -1746799553909,
|
||||
"authenticationType": "basic",
|
||||
"authentication": {
|
||||
"password": "password",
|
||||
"username": "user"
|
||||
},
|
||||
"folderId": null,
|
||||
"headers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "User-Agent",
|
||||
"value": "insomnia/11.1.0"
|
||||
}
|
||||
],
|
||||
"url": "wss://echo.websocket.org",
|
||||
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
|
||||
}
|
||||
],
|
||||
"workspaces": [
|
||||
{
|
||||
"createdAt": "2025-05-09T14:01:45.927",
|
||||
"updatedAt": "2025-05-10T02:10:54.272",
|
||||
"description": "This is the description",
|
||||
"id": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53",
|
||||
"model": "workspace",
|
||||
"name": "Dummy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
32
plugins/importer-insomnia/tests/index.test.ts
Normal file
32
plugins/importer-insomnia/tests/index.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import YAML from 'yaml';
|
||||
import { convertInsomnia } from '../src';
|
||||
|
||||
describe('importer-yaak', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.includes('.output')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test('Imports ' + fixture, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace(/.input\..*/, '.output.json')), 'utf-8');
|
||||
const result = convertInsomnia(contents);
|
||||
// console.log(JSON.stringify(result, null, 2))
|
||||
expect(result).toEqual(parseJsonOrYaml(expected));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function parseJsonOrYaml(text: string): unknown {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return YAML.parse(text);
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
44
plugins/importer-openapi/src/index.ts
Normal file
44
plugins/importer-openapi/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Context, Environment, Folder, HttpRequest, PluginDefinition, Workspace } from '@yaakapp/api';
|
||||
import { convert } from 'openapi-to-postmanv2';
|
||||
import { convertPostman } from '@yaakapp/importer-postman/src';
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
}
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'OpenAPI',
|
||||
description: 'Import OpenAPI collections',
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertOpenApi(args.text) as any;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function convertOpenApi(
|
||||
contents: string,
|
||||
): Promise<{ resources: ExportResources } | undefined> {
|
||||
let postmanCollection;
|
||||
try {
|
||||
postmanCollection = await new Promise((resolve, reject) => {
|
||||
convert({ type: 'string', data: contents }, {}, (err, result: any) => {
|
||||
if (err != null) reject(err);
|
||||
|
||||
if (Array.isArray(result.output) && result.output.length > 0) {
|
||||
resolve(result.output[0].data);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
// Probably not an OpenAPI file, so skip it
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return convertPostman(JSON.stringify(postmanCollection));
|
||||
}
|
||||
819
plugins/importer-openapi/tests/fixtures/petstore.yaml
vendored
Normal file
819
plugins/importer-openapi/tests/fixtures/petstore.yaml
vendored
Normal file
@@ -0,0 +1,819 @@
|
||||
openapi: 3.0.2
|
||||
servers:
|
||||
- url: /v3
|
||||
info:
|
||||
description: |-
|
||||
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
|
||||
Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
|
||||
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
|
||||
|
||||
Some useful links:
|
||||
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
|
||||
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
|
||||
version: 1.0.20-SNAPSHOT
|
||||
title: Swagger Petstore - OpenAPI 3.0
|
||||
termsOfService: 'http://swagger.io/terms/'
|
||||
contact:
|
||||
email: apiteam@swagger.io
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||
tags:
|
||||
- name: pet
|
||||
description: Everything about your Pets
|
||||
externalDocs:
|
||||
description: Find out more
|
||||
url: 'http://swagger.io'
|
||||
- name: store
|
||||
description: Access to Petstore orders
|
||||
externalDocs:
|
||||
description: Find out more about our store
|
||||
url: 'http://swagger.io'
|
||||
- name: user
|
||||
description: Operations about user
|
||||
paths:
|
||||
/pet:
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Add a new pet to the store
|
||||
description: Add a new pet to the store
|
||||
operationId: addPet
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
description: Create a new pet in the store
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
put:
|
||||
tags:
|
||||
- pet
|
||||
summary: Update an existing pet
|
||||
description: Update an existing pet by Id
|
||||
operationId: updatePet
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
'405':
|
||||
description: Validation exception
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
description: Update an existent pet in the store
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
/pet/findByStatus:
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Finds Pets by status
|
||||
description: Multiple status values can be provided with comma separated strings
|
||||
operationId: findPetsByStatus
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
description: Status values that need to be considered for filter
|
||||
required: false
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
default: available
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid status value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
/pet/findByTags:
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Finds Pets by tags
|
||||
description: >-
|
||||
Multiple tags can be provided with comma separated strings. Use tag1,
|
||||
tag2, tag3 for testing.
|
||||
operationId: findPetsByTags
|
||||
parameters:
|
||||
- name: tags
|
||||
in: query
|
||||
description: Tags to filter by
|
||||
required: false
|
||||
explode: true
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid tag value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
'/pet/{petId}':
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Find pet by ID
|
||||
description: Returns a single pet
|
||||
operationId: getPetById
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet to return
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
security:
|
||||
- api_key: []
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Updates a pet in the store with form data
|
||||
description: ''
|
||||
operationId: updatePetWithForm
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet that needs to be updated
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: name
|
||||
in: query
|
||||
description: Name of pet that needs to be updated
|
||||
schema:
|
||||
type: string
|
||||
- name: status
|
||||
in: query
|
||||
description: Status of pet that needs to be updated
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
delete:
|
||||
tags:
|
||||
- pet
|
||||
summary: Deletes a pet
|
||||
description: ''
|
||||
operationId: deletePet
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: header
|
||||
description: ''
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: petId
|
||||
in: path
|
||||
description: Pet id to delete
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid pet value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
'/pet/{petId}/uploadImage':
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: uploads an image
|
||||
description: ''
|
||||
operationId: uploadFile
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet to update
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: additionalMetadata
|
||||
in: query
|
||||
description: Additional Metadata
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/store/inventory:
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Returns pet inventories by status
|
||||
description: Returns a map of status codes to quantities
|
||||
operationId: getInventory
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
format: int32
|
||||
security:
|
||||
- api_key: []
|
||||
/store/order:
|
||||
post:
|
||||
tags:
|
||||
- store
|
||||
summary: Place an order for a pet
|
||||
description: Place a new order in the store
|
||||
operationId: placeOrder
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'405':
|
||||
description: Invalid input
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'/store/order/{orderId}':
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Find purchase order by ID
|
||||
x-swagger-router-controller: OrderController
|
||||
description: >-
|
||||
For valid response try integer IDs with value <= 5 or > 10. Other values
|
||||
will generate exceptions.
|
||||
operationId: getOrderById
|
||||
parameters:
|
||||
- name: orderId
|
||||
in: path
|
||||
description: ID of order that needs to be fetched
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
delete:
|
||||
tags:
|
||||
- store
|
||||
summary: Delete purchase order by ID
|
||||
x-swagger-router-controller: OrderController
|
||||
description: >-
|
||||
For valid response try integer IDs with value < 1000. Anything above
|
||||
1000 or nonintegers will generate API errors
|
||||
operationId: deleteOrder
|
||||
parameters:
|
||||
- name: orderId
|
||||
in: path
|
||||
description: ID of the order that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
/user:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Create user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: createUser
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
description: Created user object
|
||||
/user/createWithList:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Creates list of users with given input array
|
||||
description: 'Creates list of users with given input array'
|
||||
x-swagger-router-controller: UserController
|
||||
operationId: createUsersWithListInput
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
default:
|
||||
description: successful operation
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
/user/login:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs user into the system
|
||||
description: ''
|
||||
operationId: loginUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: query
|
||||
description: The user name for login
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: password
|
||||
in: query
|
||||
description: The password for login in clear text
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
headers:
|
||||
X-Rate-Limit:
|
||||
description: calls per hour allowed by the user
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
X-Expires-After:
|
||||
description: date in UTC when token expires
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: string
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid username/password supplied
|
||||
/user/logout:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs out current logged in user session
|
||||
description: ''
|
||||
operationId: logoutUser
|
||||
parameters: []
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
'/user/{username}':
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Get user by user name
|
||||
description: ''
|
||||
operationId: getUserByName
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: 'The name that needs to be fetched. Use user1 for testing. '
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
put:
|
||||
tags:
|
||||
- user
|
||||
summary: Update user
|
||||
x-swagger-router-controller: UserController
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: updateUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: name that needs to be updated
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
requestBody:
|
||||
description: Update an existent user in the store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
delete:
|
||||
tags:
|
||||
- user
|
||||
summary: Delete user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: deleteUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The name that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
externalDocs:
|
||||
description: Find out more about Swagger
|
||||
url: 'http://swagger.io'
|
||||
components:
|
||||
schemas:
|
||||
Order:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Order
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
petId:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 198772
|
||||
quantity:
|
||||
type: integer
|
||||
format: int32
|
||||
example: 7
|
||||
shipDate:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
description: Order Status
|
||||
enum:
|
||||
- placed
|
||||
- approved
|
||||
- delivered
|
||||
example: approved
|
||||
complete:
|
||||
type: boolean
|
||||
xml:
|
||||
name: order
|
||||
type: object
|
||||
Customer:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 100000
|
||||
username:
|
||||
type: string
|
||||
example: fehguy
|
||||
address:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Address'
|
||||
xml:
|
||||
wrapped: true
|
||||
name: addresses
|
||||
xml:
|
||||
name: customer
|
||||
type: object
|
||||
Address:
|
||||
properties:
|
||||
street:
|
||||
type: string
|
||||
example: 437 Lytton
|
||||
city:
|
||||
type: string
|
||||
example: Palo Alto
|
||||
state:
|
||||
type: string
|
||||
example: CA
|
||||
zip:
|
||||
type: string
|
||||
example: 94301
|
||||
xml:
|
||||
name: address
|
||||
type: object
|
||||
Category:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Category
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: Dogs
|
||||
xml:
|
||||
name: category
|
||||
type: object
|
||||
User:
|
||||
x-swagger-router-model: io.swagger.petstore.model.User
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
username:
|
||||
type: string
|
||||
example: theUser
|
||||
firstName:
|
||||
type: string
|
||||
example: John
|
||||
lastName:
|
||||
type: string
|
||||
example: James
|
||||
email:
|
||||
type: string
|
||||
example: john@email.com
|
||||
password:
|
||||
type: string
|
||||
example: 12345
|
||||
phone:
|
||||
type: string
|
||||
example: 12345
|
||||
userStatus:
|
||||
type: integer
|
||||
format: int32
|
||||
example: 1
|
||||
description: User Status
|
||||
xml:
|
||||
name: user
|
||||
type: object
|
||||
Tag:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Tag
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
xml:
|
||||
name: tag
|
||||
type: object
|
||||
Pet:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Pet
|
||||
required:
|
||||
- name
|
||||
- photoUrls
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
name:
|
||||
type: string
|
||||
example: doggie
|
||||
category:
|
||||
$ref: '#/components/schemas/Category'
|
||||
photoUrls:
|
||||
type: array
|
||||
xml:
|
||||
wrapped: true
|
||||
items:
|
||||
type: string
|
||||
xml:
|
||||
name: photoUrl
|
||||
tags:
|
||||
type: array
|
||||
xml:
|
||||
wrapped: true
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
xml:
|
||||
name: tag
|
||||
status:
|
||||
type: string
|
||||
description: pet status in the store
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
xml:
|
||||
name: pet
|
||||
type: object
|
||||
ApiResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
type:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
xml:
|
||||
name: '##default'
|
||||
type: object
|
||||
requestBodies:
|
||||
Pet:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
description: Pet object that needs to be added to the store
|
||||
UserArray:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
description: List of user object
|
||||
securitySchemes:
|
||||
petstore_auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
implicit:
|
||||
authorizationUrl: 'https://petstore.swagger.io/oauth/authorize'
|
||||
scopes:
|
||||
'write:pets': modify pets in your account
|
||||
'read:pets': read your pets
|
||||
api_key:
|
||||
type: apiKey
|
||||
name: api_key
|
||||
in: header
|
||||
29
plugins/importer-openapi/tests/index.test.ts
Normal file
29
plugins/importer-openapi/tests/index.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { convertOpenApi } from '../src';
|
||||
|
||||
describe('importer-openapi', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
test('Skips invalid file', async () => {
|
||||
const imported = await convertOpenApi('{}');
|
||||
expect(imported).toBeUndefined();
|
||||
});
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
test('Imports ' + fixture, async () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const imported = await convertOpenApi(contents);
|
||||
expect(imported?.resources.workspaces).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Swagger Petstore - OpenAPI 3.0',
|
||||
description: expect.stringContaining('This is a sample Pet Store Server'),
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests.length).toBe(19);
|
||||
expect(imported?.resources.folders.length).toBe(7);
|
||||
});
|
||||
}
|
||||
});
|
||||
370
plugins/importer-postman/src/index.ts
Normal file
370
plugins/importer-postman/src/index.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import {
|
||||
Context,
|
||||
Environment,
|
||||
Folder,
|
||||
HttpRequest,
|
||||
HttpRequestHeader,
|
||||
HttpUrlParameter,
|
||||
PluginDefinition,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
|
||||
const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
|
||||
const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
|
||||
const VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
}
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Postman',
|
||||
description: 'Import postman collections',
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertPostman(args.text) as any;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function convertPostman(
|
||||
contents: string,
|
||||
): { resources: ExportResources } | undefined {
|
||||
const root = parseJSONToRecord(contents);
|
||||
if (root == null) return;
|
||||
|
||||
const info = toRecord(root.info);
|
||||
const isValidSchema = VALID_SCHEMAS.includes(info.schema);
|
||||
if (!isValidSchema || !Array.isArray(root.item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const globalAuth = importAuth(root.auth);
|
||||
|
||||
const exportResources: ExportResources = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
httpRequests: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: generateId('workspace'),
|
||||
name: info.name || 'Postman Import',
|
||||
description: info.description?.content ?? info.description,
|
||||
};
|
||||
exportResources.workspaces.push(workspace);
|
||||
|
||||
// Create the base environment
|
||||
const environment: ExportResources['environments'][0] = {
|
||||
model: 'environment',
|
||||
id: generateId('environment'),
|
||||
name: 'Global Variables',
|
||||
workspaceId: workspace.id,
|
||||
variables:
|
||||
root.variable?.map((v: any) => ({
|
||||
name: v.key,
|
||||
value: v.value,
|
||||
})) ?? [],
|
||||
};
|
||||
exportResources.environments.push(environment);
|
||||
|
||||
const importItem = (v: Record<string, any>, folderId: string | null = null) => {
|
||||
if (typeof v.name === 'string' && Array.isArray(v.item)) {
|
||||
const folder: ExportResources['folders'][0] = {
|
||||
model: 'folder',
|
||||
workspaceId: workspace.id,
|
||||
id: generateId('folder'),
|
||||
name: v.name,
|
||||
folderId,
|
||||
};
|
||||
exportResources.folders.push(folder);
|
||||
for (const child of v.item) {
|
||||
importItem(child, folder.id);
|
||||
}
|
||||
} else if (typeof v.name === 'string' && 'request' in v) {
|
||||
const r = toRecord(v.request);
|
||||
const bodyPatch = importBody(r.body);
|
||||
const requestAuthPath = importAuth(r.auth);
|
||||
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
|
||||
|
||||
const headers: HttpRequestHeader[] = toArray(r.header).map((h) => {
|
||||
return {
|
||||
name: h.key,
|
||||
value: h.value,
|
||||
enabled: !h.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
// Add body headers only if they don't already exist
|
||||
for (const bodyPatchHeader of bodyPatch.headers) {
|
||||
const existingHeader = headers.find(h => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase());
|
||||
if (existingHeader) {
|
||||
continue;
|
||||
}
|
||||
headers.push(bodyPatchHeader);
|
||||
}
|
||||
|
||||
const { url, urlParameters } = convertUrl(r.url);
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
model: 'http_request',
|
||||
id: generateId('http_request'),
|
||||
workspaceId: workspace.id,
|
||||
folderId,
|
||||
name: v.name,
|
||||
description: v.description || undefined,
|
||||
method: r.method || 'GET',
|
||||
url,
|
||||
urlParameters,
|
||||
body: bodyPatch.body,
|
||||
bodyType: bodyPatch.bodyType,
|
||||
authentication: authPatch.authentication,
|
||||
authenticationType: authPatch.authenticationType,
|
||||
headers,
|
||||
};
|
||||
exportResources.httpRequests.push(request);
|
||||
} else {
|
||||
console.log('Unknown item', v, folderId);
|
||||
}
|
||||
};
|
||||
|
||||
for (const item of root.item) {
|
||||
importItem(item);
|
||||
}
|
||||
|
||||
const resources = deleteUndefinedAttrs(convertTemplateSyntax(exportResources));
|
||||
|
||||
return { resources };
|
||||
}
|
||||
|
||||
function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters'> {
|
||||
if (typeof url === 'string') {
|
||||
return { url, urlParameters: [] };
|
||||
}
|
||||
|
||||
url = toRecord(url);
|
||||
|
||||
let v = '';
|
||||
|
||||
if ('protocol' in url && typeof url.protocol === 'string') {
|
||||
v += `${url.protocol}://`;
|
||||
}
|
||||
|
||||
if ('host' in url) {
|
||||
v += `${Array.isArray(url.host) ? url.host.join('.') : url.host}`;
|
||||
}
|
||||
|
||||
if ('port' in url && typeof url.port === 'string') {
|
||||
v += `:${url.port}`;
|
||||
}
|
||||
|
||||
if ('path' in url && Array.isArray(url.path) && url.path.length > 0) {
|
||||
v += `/${Array.isArray(url.path) ? url.path.join('/') : url.path}`;
|
||||
}
|
||||
|
||||
const params: HttpUrlParameter[] = [];
|
||||
if ('query' in url && Array.isArray(url.query) && url.query.length > 0) {
|
||||
for (const query of url.query) {
|
||||
params.push({
|
||||
name: query.key ?? '',
|
||||
value: query.value ?? '',
|
||||
enabled: !query.disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('variable' in url && Array.isArray(url.variable) && url.variable.length > 0) {
|
||||
for (const v of url.variable) {
|
||||
params.push({
|
||||
name: ':' + (v.key ?? ''),
|
||||
value: v.value ?? '',
|
||||
enabled: !v.disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('hash' in url && typeof url.hash === 'string') {
|
||||
v += `#${url.hash}`;
|
||||
}
|
||||
|
||||
// TODO: Implement url.variables (path variables)
|
||||
|
||||
return { url: v, urlParameters: params };
|
||||
}
|
||||
|
||||
function importAuth(
|
||||
rawAuth: any,
|
||||
): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
|
||||
const auth = toRecord(rawAuth);
|
||||
if ('basic' in auth) {
|
||||
return {
|
||||
authenticationType: 'basic',
|
||||
authentication: {
|
||||
username: auth.basic.username || '',
|
||||
password: auth.basic.password || '',
|
||||
},
|
||||
};
|
||||
} else if ('bearer' in auth) {
|
||||
return {
|
||||
authenticationType: 'bearer',
|
||||
authentication: {
|
||||
token: auth.bearer.token || '',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { authenticationType: null, authentication: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
|
||||
const body = toRecord(rawBody);
|
||||
if (body.mode === 'graphql') {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'graphql',
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: body.graphql.query, variables: parseJSONToRecord(body.graphql.variables) },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
};
|
||||
} else if (body.mode === 'urlencoded') {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: toArray(body.urlencoded).map((f) => ({
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
})),
|
||||
},
|
||||
};
|
||||
} else if (body.mode === 'formdata') {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: toArray(body.formdata).map((f) =>
|
||||
f.src != null
|
||||
? {
|
||||
enabled: !f.disabled,
|
||||
contentType: f.contentType ?? null,
|
||||
name: f.key ?? '',
|
||||
file: f.src ?? '',
|
||||
}
|
||||
: {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
} else if (body.mode === 'raw') {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: body.options?.raw?.language === 'json' ? 'application/json' : '',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other',
|
||||
body: {
|
||||
text: body.raw ?? '',
|
||||
},
|
||||
};
|
||||
} else if (body.mode === 'file') {
|
||||
return {
|
||||
headers: [],
|
||||
bodyType: 'binary',
|
||||
body: {
|
||||
filePath: body.file?.src,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { headers: [], bodyType: null, body: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function parseJSONToRecord(jsonStr: string): Record<string, any> | null {
|
||||
try {
|
||||
return toRecord(JSON.parse(jsonStr));
|
||||
} catch (err) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toRecord(value: any): Record<string, any> {
|
||||
if (Object.prototype.toString.call(value) === '[object Object]') return value;
|
||||
else return {};
|
||||
}
|
||||
|
||||
function toArray(value: any): any[] {
|
||||
if (Object.prototype.toString.call(value) === '[object Array]') return value;
|
||||
else return [];
|
||||
}
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
|
||||
} else if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
} else if (typeof obj === 'object' && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs) as T;
|
||||
} else if (typeof obj === 'object' && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
|
||||
) as T;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
const idCount: Partial<Record<string, number>> = {};
|
||||
|
||||
function generateId(model: string): string {
|
||||
idCount[model] = (idCount[model] ?? -1) + 1;
|
||||
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
|
||||
}
|
||||
38
plugins/importer-postman/tests/fixtures/nested.input.json
vendored
Normal file
38
plugins/importer-postman/tests/fixtures/nested.input.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
|
||||
"_exporter_id": "18798"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Top Folder",
|
||||
"item": [
|
||||
{
|
||||
"name": "Nested Folder",
|
||||
"item": [
|
||||
{
|
||||
"name": "Request 1",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Request 2",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Request 3",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
83
plugins/importer-postman/tests/fixtures/nested.output.json
vendored
Normal file
83
plugins/importer-postman/tests/fixtures/nested.output.json
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"resources": {
|
||||
"workspaces": [
|
||||
{
|
||||
"model": "workspace",
|
||||
"id": "GENERATE_ID::WORKSPACE_0",
|
||||
"name": "New Collection"
|
||||
}
|
||||
],
|
||||
"environments": [
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_0",
|
||||
"model": "environment",
|
||||
"name": "Global Variables",
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0"
|
||||
}
|
||||
],
|
||||
"httpRequests": [
|
||||
{
|
||||
"model": "http_request",
|
||||
"id": "GENERATE_ID::HTTP_REQUEST_0",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
"folderId": "GENERATE_ID::FOLDER_1",
|
||||
"name": "Request 1",
|
||||
"method": "GET",
|
||||
"url": "",
|
||||
"urlParameters": [],
|
||||
"body": {},
|
||||
"bodyType": null,
|
||||
"authentication": {},
|
||||
"authenticationType": null,
|
||||
"headers": []
|
||||
},
|
||||
{
|
||||
"model": "http_request",
|
||||
"id": "GENERATE_ID::HTTP_REQUEST_1",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
"folderId": "GENERATE_ID::FOLDER_0",
|
||||
"name": "Request 2",
|
||||
"method": "GET",
|
||||
"url": "",
|
||||
"urlParameters": [],
|
||||
"body": {},
|
||||
"bodyType": null,
|
||||
"authentication": {},
|
||||
"authenticationType": null,
|
||||
"headers": []
|
||||
},
|
||||
{
|
||||
"model": "http_request",
|
||||
"id": "GENERATE_ID::HTTP_REQUEST_2",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
"folderId": null,
|
||||
"name": "Request 3",
|
||||
"method": "GET",
|
||||
"url": "",
|
||||
"urlParameters": [],
|
||||
"body": {},
|
||||
"bodyType": null,
|
||||
"authentication": {},
|
||||
"authenticationType": null,
|
||||
"headers": []
|
||||
}
|
||||
],
|
||||
"folders": [
|
||||
{
|
||||
"model": "folder",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
"id": "GENERATE_ID::FOLDER_0",
|
||||
"name": "Top Folder",
|
||||
"folderId": null
|
||||
},
|
||||
{
|
||||
"model": "folder",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
"id": "GENERATE_ID::FOLDER_1",
|
||||
"name": "Nested Folder",
|
||||
"folderId": "GENERATE_ID::FOLDER_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
136
plugins/importer-postman/tests/fixtures/params.input.json
vendored
Normal file
136
plugins/importer-postman/tests/fixtures/params.input.json
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "18798"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Form URL",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "baeare",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-foo",
|
||||
"value": "bar",
|
||||
"description": "description"
|
||||
},
|
||||
{
|
||||
"key": "Disabled",
|
||||
"value": "tnroant",
|
||||
"description": "ntisorantosra",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "Key",
|
||||
"contentType": "Custom/COntent",
|
||||
"description": "DEscription",
|
||||
"type": "file",
|
||||
"src": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11 PM.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "example.com/:foo/:bar?q=qqq&",
|
||||
"host": [
|
||||
"example",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":foo",
|
||||
":bar"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "disabled",
|
||||
"value": "secondvalue",
|
||||
"description": "this is disabled",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "q",
|
||||
"value": "qqq",
|
||||
"description": "hello"
|
||||
},
|
||||
{
|
||||
"key": "",
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "foo",
|
||||
"value": "fff",
|
||||
"description": "Description"
|
||||
},
|
||||
{
|
||||
"key": "bar",
|
||||
"value": "bbb",
|
||||
"description": "bbb description"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "password",
|
||||
"value": "globalpass",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "globaluser",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"packages": {},
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"packages": {},
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "COLLECTION VARIABLE",
|
||||
"value": "collection variable",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
96
plugins/importer-postman/tests/fixtures/params.output.json
vendored
Normal file
96
plugins/importer-postman/tests/fixtures/params.output.json
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"resources": {
|
||||
"workspaces": [
|
||||
{
|
||||
"model": "workspace",
|
||||
"id": "GENERATE_ID::WORKSPACE_1",
|
||||
"name": "New Collection"
|
||||
}
|
||||
],
|
||||
"environments": [
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_1",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_1",
|
||||
"model": "environment",
|
||||
"name": "Global Variables",
|
||||
"variables": [
|
||||
{
|
||||
"name": "COLLECTION VARIABLE",
|
||||
"value": "collection variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"httpRequests": [
|
||||
{
|
||||
"model": "http_request",
|
||||
"id": "GENERATE_ID::HTTP_REQUEST_3",
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_1",
|
||||
"folderId": null,
|
||||
"name": "Form URL",
|
||||
"method": "POST",
|
||||
"url": "example.com/:foo/:bar",
|
||||
"urlParameters": [
|
||||
{
|
||||
"name": "disabled",
|
||||
"value": "secondvalue",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "q",
|
||||
"value": "qqq",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": ":foo",
|
||||
"value": "fff",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": ":bar",
|
||||
"value": "bbb",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"form": [
|
||||
{
|
||||
"enabled": true,
|
||||
"contentType": "Custom/COntent",
|
||||
"name": "Key",
|
||||
"file": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11 PM.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bodyType": "multipart/form-data",
|
||||
"authentication": {
|
||||
"token": ""
|
||||
},
|
||||
"authenticationType": "bearer",
|
||||
"headers": [
|
||||
{
|
||||
"name": "X-foo",
|
||||
"value": "bar",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "Disabled",
|
||||
"value": "tnroant",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/form-data",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"folders": []
|
||||
}
|
||||
}
|
||||
23
plugins/importer-postman/tests/index.test.ts
Normal file
23
plugins/importer-postman/tests/index.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { convertPostman } from '../src';
|
||||
|
||||
describe('importer-postman', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.includes('.output')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test('Imports ' + fixture, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
|
||||
const result = convertPostman(contents);
|
||||
// console.log(JSON.stringify(result, null, 2))
|
||||
expect(result).toEqual(JSON.parse(expected));
|
||||
});
|
||||
}
|
||||
});
|
||||
74
plugins/importer-yaak/src/index.ts
Normal file
74
plugins/importer-yaak/src/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Environment, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Yaak',
|
||||
description: 'Yaak official format',
|
||||
onImport(_ctx, args) {
|
||||
return migrateImport(args.text) as any;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function migrateImport(contents: string) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isJSObject(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isYaakExport = 'yaakSchema' in parsed;
|
||||
if (!isYaakExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate v1 to v2 -- changes requests to httpRequests
|
||||
if ('requests' in parsed.resources) {
|
||||
parsed.resources.httpRequests = parsed.resources.requests;
|
||||
delete parsed.resources['requests'];
|
||||
}
|
||||
|
||||
// Migrate v2 to v3
|
||||
for (const workspace of parsed.resources.workspaces ?? []) {
|
||||
if ('variables' in workspace) {
|
||||
// Create the base environment
|
||||
const baseEnvironment: Partial<Environment> = {
|
||||
id: `GENERATE_ID::base_env_${workspace['id']}`,
|
||||
name: 'Global Variables',
|
||||
variables: workspace.variables,
|
||||
workspaceId: workspace.id,
|
||||
};
|
||||
parsed.resources.environments = parsed.resources.environments ?? [];
|
||||
parsed.resources.environments.push(baseEnvironment);
|
||||
|
||||
// Delete variables key from the workspace
|
||||
delete workspace.variables;
|
||||
|
||||
// Add environmentId to relevant environments
|
||||
for (const environment of parsed.resources.environments) {
|
||||
if (environment.workspaceId === workspace.id && environment.id !== baseEnvironment.id) {
|
||||
environment.environmentId = baseEnvironment.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate v3 to v4
|
||||
for (const environment of parsed.resources.environments ?? []) {
|
||||
if ('environmentId' in environment) {
|
||||
environment.base = environment.environmentId == null;
|
||||
delete environment.environmentId;
|
||||
}
|
||||
}
|
||||
|
||||
return { resources: parsed.resources }; // Should already be in the correct format
|
||||
}
|
||||
|
||||
function isJSObject(obj: any) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
}
|
||||
70
plugins/importer-yaak/tests/index.test.ts
Normal file
70
plugins/importer-yaak/tests/index.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { migrateImport } from '../src';
|
||||
|
||||
describe('importer-yaak', () => {
|
||||
test('Skips invalid imports', () => {
|
||||
expect(migrateImport('not JSON')).toBeUndefined();
|
||||
expect(migrateImport('[]')).toBeUndefined();
|
||||
expect(migrateImport(JSON.stringify({ resources: {} }))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('converts schema 1 to 2', () => {
|
||||
const imported = migrateImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 1,
|
||||
resources: {
|
||||
requests: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported).toEqual(
|
||||
expect.objectContaining({
|
||||
resources: {
|
||||
httpRequests: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('converts schema 2 to 3', () => {
|
||||
const imported = migrateImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 2,
|
||||
resources: {
|
||||
environments: [{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
}],
|
||||
workspaces: [{
|
||||
id: 'w_1',
|
||||
variables: [{ name: 'W1', value: 'W1!' }],
|
||||
}],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported).toEqual(
|
||||
expect.objectContaining({
|
||||
resources: {
|
||||
workspaces: [{
|
||||
id: 'w_1',
|
||||
}],
|
||||
environments: [{
|
||||
id: 'e_1',
|
||||
base: false,
|
||||
workspaceId: 'w_1',
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
}, {
|
||||
id: 'GENERATE_ID::base_env_w_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Global Variables',
|
||||
variables: [{ name: 'W1', value: 'W1!' }],
|
||||
}],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
9
plugins/template-function-cookie/package.json
Normal file
9
plugins/template-function-cookie/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-cookie",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
}
|
||||
}
|
||||
20
plugins/template-function-cookie/src/index.ts
Normal file
20
plugins/template-function-cookie/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'cookie.value',
|
||||
description: 'Read the value of a cookie in the jar, by name',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'cookie_name',
|
||||
label: 'Cookie Name',
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
9
plugins/template-function-encode/package.json
Normal file
9
plugins/template-function-encode/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-encode",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
}
|
||||
}
|
||||
42
plugins/template-function-encode/src/index.ts
Normal file
42
plugins/template-function-encode/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'base64.encode',
|
||||
description: 'Encode a value to base64',
|
||||
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return Buffer.from(args.values.value ?? '').toString('base64');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'base64.decode',
|
||||
description: 'Decode a value from base64',
|
||||
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return Buffer.from(args.values.value ?? '', 'base64').toString('utf-8');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'url.encode',
|
||||
description: 'Encode a value for use in a URL (percent-encoding)',
|
||||
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return encodeURIComponent(args.values.value ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'url.decode',
|
||||
description: 'Decode a percent-encoded URL value',
|
||||
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
return decodeURIComponent(args.values.value ?? '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
19
plugins/template-function-fs/src/index.ts
Normal file
19
plugins/template-function-fs/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [{
|
||||
name: 'fs.readFile',
|
||||
description: 'Read the contents of a file as utf-8',
|
||||
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.path) return null;
|
||||
|
||||
try {
|
||||
return fs.promises.readFile(args.values.path, 'utf-8');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
86
plugins/template-function-hash/src/index.ts
Executable file
86
plugins/template-function-hash/src/index.ts
Executable file
@@ -0,0 +1,86 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
|
||||
const algorithms = ['md5', 'sha1', 'sha256', 'sha512'] as const;
|
||||
const encodings = ['base64', 'hex'] as const;
|
||||
|
||||
type TemplateFunctionPlugin = NonNullable<PluginDefinition['templateFunctions']>[number];
|
||||
|
||||
const hashFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({
|
||||
name: `hash.${algorithm}`,
|
||||
description: 'Hash a value to its hexidecimal representation',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
placeholder: 'input text',
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
defaultValue: 'base64',
|
||||
options: encodings.map(encoding => ({
|
||||
label: capitalize(encoding),
|
||||
value: encoding,
|
||||
})),
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input);
|
||||
const encoding = String(args.values.encoding) as typeof encodings[number];
|
||||
|
||||
return createHash(algorithm)
|
||||
.update(input, 'utf-8')
|
||||
.digest(encoding);
|
||||
},
|
||||
}));
|
||||
|
||||
const hmacFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({
|
||||
name: `hmac.${algorithm}`,
|
||||
description: 'Compute the HMAC of a value',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
placeholder: 'input text',
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'key',
|
||||
label: 'Key',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
defaultValue: 'base64',
|
||||
options: encodings.map(encoding => ({
|
||||
value: encoding,
|
||||
label: capitalize(encoding),
|
||||
})),
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input);
|
||||
const key = String(args.values.key);
|
||||
const encoding = String(args.values.encoding) as typeof encodings[number];
|
||||
|
||||
return createHmac(algorithm, key, {})
|
||||
.update(input)
|
||||
.digest(encoding);
|
||||
},
|
||||
}));
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [...hashFunctions, ...hmacFunctions],
|
||||
};
|
||||
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
15
plugins/template-function-json/package.json
Executable file
15
plugins/template-function-json/package.json
Executable file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-json",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
}
|
||||
}
|
||||
48
plugins/template-function-json/src/index.ts
Executable file
48
plugins/template-function-json/src/index.ts
Executable file
@@ -0,0 +1,48 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'json.jsonpath',
|
||||
description: 'Filter JSON-formatted text using JSONPath syntax',
|
||||
args: [
|
||||
{ type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: '{ "foo": "bar" }' },
|
||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
|
||||
{ type: 'checkbox', name: 'formatted', label: 'Format Output' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
const parsed = JSON.parse(String(args.values.input));
|
||||
const query = String(args.values.query ?? '$').trim();
|
||||
let filtered = JSONPath({ path: query, json: parsed });
|
||||
if (Array.isArray(filtered)) {
|
||||
filtered = filtered[0];
|
||||
}
|
||||
if (typeof filtered === 'string') {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (args.values.formatted) {
|
||||
return JSON.stringify(filtered, null, 2);
|
||||
} else {
|
||||
return JSON.stringify(filtered);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'json.escape',
|
||||
description: 'Escape a JSON string, useful when using the output in JSON values',
|
||||
args: [
|
||||
{ type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: 'Hello "World"' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
25
plugins/template-function-prompt/src/index.ts
Normal file
25
plugins/template-function-prompt/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [{
|
||||
name: 'prompt.text',
|
||||
description: 'Prompt the user for input when sending a request',
|
||||
args: [
|
||||
{ type: 'text', name: 'title', label: 'Title' },
|
||||
{ type: 'text', name: 'label', label: 'Label', optional: true },
|
||||
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
|
||||
{ type: 'text', name: 'placeholder', label: 'Placeholder', optional: true },
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (args.purpose !== 'send') return null;
|
||||
|
||||
return await ctx.prompt.text({
|
||||
id: `prompt-${args.values.label}`,
|
||||
label: args.values.title ?? '',
|
||||
title: args.values.title ?? '',
|
||||
defaultValue: args.values.defaultValue,
|
||||
placeholder: args.values.placeholder,
|
||||
});
|
||||
},
|
||||
}],
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-file",
|
||||
"name": "@yaakapp/template-function-regex",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
28
plugins/template-function-regex/src/index.ts
Normal file
28
plugins/template-function-regex/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [{
|
||||
name: 'regex.match',
|
||||
description: 'Extract',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'regex',
|
||||
label: 'Regular Expression',
|
||||
placeholder: '^\w+=(?<value>\w*)$',
|
||||
defaultValue: '^(.*)$',
|
||||
description: 'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.',
|
||||
},
|
||||
{ type: 'text', name: 'input', label: 'Input Text', multiLine: true },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.regex) return '';
|
||||
|
||||
const regex = new RegExp(String(args.values.regex));
|
||||
const match = args.values.input?.match(regex);
|
||||
return match?.groups
|
||||
? Object.values(match.groups)[0] ?? ''
|
||||
: match?.[1] ?? match?.[0] ?? '';
|
||||
},
|
||||
}],
|
||||
};
|
||||
45
plugins/template-function-request/src/index.ts
Executable file
45
plugins/template-function-request/src/index.ts
Executable file
@@ -0,0 +1,45 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'request.body',
|
||||
args: [{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
}],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' });
|
||||
if (httpRequest == null) return null;
|
||||
return String(await ctx.templates.render({
|
||||
data: httpRequest.body?.text ?? '',
|
||||
purpose: args.purpose,
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request.header',
|
||||
args: [
|
||||
{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
},
|
||||
{
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
type: 'text',
|
||||
}],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' });
|
||||
if (httpRequest == null) return null;
|
||||
const header = httpRequest.headers.find(h => h.name.toLowerCase() === args.values.header?.toLowerCase());
|
||||
return String(await ctx.templates.render({
|
||||
data: header?.value ?? '',
|
||||
purpose: args.purpose,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^9.0.0",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"xpath": "^0.0.34",
|
||||
"@xmldom/xmldom": "^0.8.10"
|
||||
},
|
||||
210
plugins/template-function-response/src/index.ts
Normal file
210
plugins/template-function-response/src/index.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import {
|
||||
CallTemplateFunctionArgs,
|
||||
Context,
|
||||
FormInput,
|
||||
HttpResponse,
|
||||
PluginDefinition,
|
||||
RenderPurpose,
|
||||
} from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import xpath from 'xpath';
|
||||
|
||||
const behaviorArg: FormInput = {
|
||||
type: 'select',
|
||||
name: 'behavior',
|
||||
label: 'Sending Behavior',
|
||||
defaultValue: 'smart',
|
||||
options: [
|
||||
{ label: 'When no responses', value: 'smart' },
|
||||
{ label: 'Always', value: 'always' },
|
||||
],
|
||||
};
|
||||
|
||||
const requestArg: FormInput = {
|
||||
type: 'http_request',
|
||||
name: 'request',
|
||||
label: 'Request',
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'response.header',
|
||||
description: 'Read the value of a response header, by name',
|
||||
args: [
|
||||
requestArg,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
placeholder: 'Content-Type',
|
||||
},
|
||||
behaviorArg,
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.request || !args.values.header) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: args.values.request,
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ?? null,
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
const header = response.headers.find(
|
||||
h => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(),
|
||||
);
|
||||
return header?.value ?? null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'response.body.path',
|
||||
description: 'Access a field of the response body using JsonPath or XPath',
|
||||
aliases: ['response'],
|
||||
args: [
|
||||
requestArg,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'path',
|
||||
label: 'JSONPath or XPath',
|
||||
placeholder: '$.books[0].id or /books[0]/id',
|
||||
},
|
||||
behaviorArg,
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.request || !args.values.path) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: args.values.request,
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ?? null,
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
if (response.bodyPath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = readFileSync(response.bodyPath, 'utf-8');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return filterJSONPath(body, args.values.path);
|
||||
} catch (err) {
|
||||
// Probably not JSON, try XPath
|
||||
}
|
||||
|
||||
try {
|
||||
return filterXPath(body, args.values.path);
|
||||
} catch (err) {
|
||||
// Probably not XML
|
||||
}
|
||||
|
||||
return null; // Bail out
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'response.body.raw',
|
||||
description: 'Access the entire response body, as text',
|
||||
aliases: ['response'],
|
||||
args: [
|
||||
requestArg,
|
||||
behaviorArg,
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.request) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: args.values.request,
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ?? null,
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
if (response.bodyPath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = readFileSync(response.bodyPath, 'utf-8');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function filterJSONPath(body: string, path: string): string {
|
||||
const parsed = JSON.parse(body);
|
||||
const items = JSONPath({ path, json: parsed })[0];
|
||||
if (items == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.toString.call(items) === '[object Array]' ||
|
||||
Object.prototype.toString.call(items) === '[object Object]'
|
||||
) {
|
||||
return JSON.stringify(items);
|
||||
} else {
|
||||
return String(items);
|
||||
}
|
||||
}
|
||||
|
||||
function filterXPath(body: string, path: string): string {
|
||||
const doc = new DOMParser().parseFromString(body, 'text/xml');
|
||||
const items = xpath.select(path, doc, false);
|
||||
|
||||
if (Array.isArray(items)) {
|
||||
return items[0] != null ? String(items[0].firstChild ?? '') : '';
|
||||
} else {
|
||||
// Not sure what cases this happens in (?)
|
||||
return String(items);
|
||||
}
|
||||
}
|
||||
|
||||
async function getResponse(ctx: Context, { requestId, behavior, purpose }: {
|
||||
requestId: string,
|
||||
behavior: string | null,
|
||||
purpose: RenderPurpose,
|
||||
}): Promise<HttpResponse | null> {
|
||||
if (!requestId) return null;
|
||||
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' });
|
||||
if (httpRequest == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const responses = await ctx.httpResponse.find({ requestId: httpRequest.id, limit: 1 });
|
||||
|
||||
if (behavior === 'never' && responses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let response: HttpResponse | null = responses[0] ?? null;
|
||||
|
||||
// Previews happen a ton, and we don't want to send too many times on "always," so treat
|
||||
// it as "smart" during preview.
|
||||
let finalBehavior = (behavior === 'always' && purpose === 'preview')
|
||||
? 'smart'
|
||||
: behavior;
|
||||
|
||||
// Send if no responses and "smart," or "always"
|
||||
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') {
|
||||
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
|
||||
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
|
||||
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
12
plugins/template-function-uuid/package.json
Normal file
12
plugins/template-function-uuid/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-uuid",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
76
plugins/template-function-uuid/src/index.ts
Normal file
76
plugins/template-function-uuid/src/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { v1, v3, v4, v5, v6, v7 } from 'uuid';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'uuid.v1',
|
||||
description: 'Generate a UUID V1',
|
||||
args: [],
|
||||
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v1();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v3',
|
||||
description: 'Generate a UUID V3',
|
||||
args: [
|
||||
{ type: 'text', name: 'name', label: 'Name' },
|
||||
{
|
||||
type: 'text',
|
||||
name: 'namespace',
|
||||
label: 'Namespace UUID',
|
||||
description: 'A valid UUID to use as the namespace',
|
||||
placeholder: '24ced880-3bf4-11f0-8329-cd053d577f0e',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v3(String(args.values.name), String(args.values.namespace));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v4',
|
||||
description: 'Generate a UUID V4',
|
||||
args: [],
|
||||
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v4();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v5',
|
||||
description: 'Generate a UUID V5',
|
||||
args: [
|
||||
{ type: 'text', name: 'name', label: 'Name' },
|
||||
{ type: 'text', name: 'namespace', label: 'Namespace' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v5(String(args.values.name), String(args.values.namespace));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v6',
|
||||
description: 'Generate a UUID V6',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'timestamp',
|
||||
label: 'Timestamp',
|
||||
optional: true,
|
||||
description: 'Can be any format that can be parsed by JavaScript new Date(...)',
|
||||
placeholder: '2025-05-28T11:15:00Z',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v6({ msecs: new Date(String(args.values.timestamp)).getTime() });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v7',
|
||||
description: 'Generate a UUID V7',
|
||||
args: [],
|
||||
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v7();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
13
plugins/template-function-xml/package.json
Executable file
13
plugins/template-function-xml/package.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@yaakapp/template-function-xml",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "yaakcli build ./src/index.ts",
|
||||
"dev": "yaakcli dev ./src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "^0.0.34"
|
||||
}
|
||||
}
|
||||
29
plugins/template-function-xml/src/index.ts
Executable file
29
plugins/template-function-xml/src/index.ts
Executable file
@@ -0,0 +1,29 @@
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import xpath from 'xpath';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [{
|
||||
name: 'xml.xpath',
|
||||
description: 'Filter XML-formatted text using XPath syntax',
|
||||
args: [
|
||||
{ type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: '<foo></foo>' },
|
||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '//foo' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(String(args.values.input), 'text/xml');
|
||||
let result = xpath.select(String(args.values.query), doc, false);
|
||||
if (Array.isArray(result)) {
|
||||
return String(result.map(c => String(c.firstChild))[0] ?? '');
|
||||
} else if (result instanceof Node) {
|
||||
return String(result.firstChild);
|
||||
} else {
|
||||
return String(result);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
|
||||
# Widths
|
||||
chain_width = 100
|
||||
|
||||
46
scripts/create-migration.cjs
Normal file
46
scripts/create-migration.cjs
Normal file
@@ -0,0 +1,46 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
const slugify = require('slugify');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
function generateTimestamp() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
async function createMigration() {
|
||||
try {
|
||||
const migrationName = await new Promise((resolve) => {
|
||||
rl.question('Enter migration name: ', resolve);
|
||||
});
|
||||
|
||||
const timestamp = generateTimestamp();
|
||||
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
|
||||
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
|
||||
const filePath = path.join(migrationsDir, fileName);
|
||||
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
fs.mkdirSync(migrationsDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, '-- Add migration SQL here\n');
|
||||
console.log(`Created migration file: ${fileName}`);
|
||||
} catch (error) {
|
||||
console.error('Error creating migration:', error);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
createMigration().catch(console.error);
|
||||
@@ -9,12 +9,14 @@ const NODE_VERSION = 'v22.9.0';
|
||||
// `${process.platform}_${process.arch}`
|
||||
const MAC_ARM = 'darwin_arm64';
|
||||
const MAC_X64 = 'darwin_x64';
|
||||
const LNX_ARM = 'linux_arm64';
|
||||
const LNX_X64 = 'linux_x64';
|
||||
const WIN_X64 = 'win32_x64';
|
||||
|
||||
const URL_MAP = {
|
||||
[MAC_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||||
[MAC_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-x64.tar.gz`,
|
||||
[LNX_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz`,
|
||||
[LNX_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz`,
|
||||
[WIN_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-win-x64.zip`,
|
||||
};
|
||||
@@ -22,15 +24,17 @@ const URL_MAP = {
|
||||
const SRC_BIN_MAP = {
|
||||
[MAC_ARM]: `node-${NODE_VERSION}-darwin-arm64/bin/node`,
|
||||
[MAC_X64]: `node-${NODE_VERSION}-darwin-x64/bin/node`,
|
||||
[LNX_ARM]: `node-${NODE_VERSION}-linux-arm64/bin/node`,
|
||||
[LNX_X64]: `node-${NODE_VERSION}-linux-x64/bin/node`,
|
||||
[WIN_X64]: `node-${NODE_VERSION}-win-x64/node.exe`,
|
||||
};
|
||||
|
||||
const DST_BIN_MAP = {
|
||||
darwin_arm64: 'yaaknode-aarch64-apple-darwin',
|
||||
darwin_x64: 'yaaknode-x86_64-apple-darwin',
|
||||
linux_x64: 'yaaknode-x86_64-unknown-linux-gnu',
|
||||
win32_x64: 'yaaknode-x86_64-pc-windows-msvc.exe',
|
||||
[MAC_ARM]: 'yaaknode-aarch64-apple-darwin',
|
||||
[MAC_X64]: 'yaaknode-x86_64-apple-darwin',
|
||||
[LNX_ARM]: 'yaaknode-aarch64-unknown-linux-gnu',
|
||||
[LNX_X64]: 'yaaknode-x86_64-unknown-linux-gnu',
|
||||
[WIN_X64]: 'yaaknode-x86_64-pc-windows-msvc.exe',
|
||||
};
|
||||
|
||||
const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`;
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
const { readdirSync, cpSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
|
||||
if (!pluginsDir) {
|
||||
console.log('Skipping bundled plugins build because YAAK_PLUGINS_DIR is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Installing Yaak plugins dependencies', pluginsDir);
|
||||
execSync('npm ci', { cwd: pluginsDir });
|
||||
console.log('Building Yaak plugins', pluginsDir);
|
||||
execSync('npm run build', { cwd: pluginsDir });
|
||||
const pluginsDir = path.join(__dirname, '..', 'plugins');
|
||||
|
||||
console.log('Copying Yaak plugins to', pluginsDir);
|
||||
|
||||
const pluginsRoot = path.join(pluginsDir, 'plugins');
|
||||
for (const name of readdirSync(pluginsRoot)) {
|
||||
const dir = path.join(pluginsRoot, name);
|
||||
for (const name of readdirSync(pluginsDir)) {
|
||||
const dir = path.join(pluginsDir, name);
|
||||
if (name.startsWith('.')) continue;
|
||||
console.log('Building plugin', dir);
|
||||
execSync('npm run build', { cwd: dir });
|
||||
const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name);
|
||||
console.log(`Copying ${name} to ${destDir}`);
|
||||
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
||||
|
||||
@@ -9,12 +9,14 @@ const VERSION = '28.3';
|
||||
// `${process.platform}_${process.arch}`
|
||||
const MAC_ARM = 'darwin_arm64';
|
||||
const MAC_X64 = 'darwin_x64';
|
||||
const LNX_ARM = 'linux_arm64';
|
||||
const LNX_X64 = 'linux_x64';
|
||||
const WIN_X64 = 'win32_x64';
|
||||
|
||||
const URL_MAP = {
|
||||
[MAC_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-aarch_64.zip`,
|
||||
[MAC_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-x86_64.zip`,
|
||||
[LNX_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-aarch_64.zip`,
|
||||
[LNX_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-x86_64.zip`,
|
||||
[WIN_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-win64.zip`,
|
||||
};
|
||||
@@ -22,6 +24,7 @@ const URL_MAP = {
|
||||
const SRC_BIN_MAP = {
|
||||
[MAC_ARM]: 'bin/protoc',
|
||||
[MAC_X64]: 'bin/protoc',
|
||||
[LNX_ARM]: 'bin/protoc',
|
||||
[LNX_X64]: 'bin/protoc',
|
||||
[WIN_X64]: 'bin/protoc.exe',
|
||||
};
|
||||
@@ -29,6 +32,7 @@ const SRC_BIN_MAP = {
|
||||
const DST_BIN_MAP = {
|
||||
[MAC_ARM]: 'yaakprotoc-aarch64-apple-darwin',
|
||||
[MAC_X64]: 'yaakprotoc-x86_64-apple-darwin',
|
||||
[LNX_ARM]: 'yaakprotoc-aarch64-unknown-linux-gnu',
|
||||
[LNX_X64]: 'yaakprotoc-x86_64-unknown-linux-gnu',
|
||||
[WIN_X64]: 'yaakprotoc-x86_64-pc-windows-msvc.exe',
|
||||
};
|
||||
|
||||
6
src-tauri/.gitignore
vendored
6
src-tauri/.gitignore
vendored
@@ -3,4 +3,8 @@
|
||||
target/
|
||||
|
||||
vendored/*
|
||||
!vendored/plugins
|
||||
|
||||
gen/*
|
||||
|
||||
**/permissions/autogenerated
|
||||
**/permissions/schemas
|
||||
|
||||
3954
src-tauri/Cargo.lock
generated
3954
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,12 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"yaak-crypto",
|
||||
"yaak-git",
|
||||
"yaak-grpc",
|
||||
"yaak-fonts",
|
||||
"yaak-http",
|
||||
"yaak-license",
|
||||
"yaak-mac-window",
|
||||
"yaak-models",
|
||||
"yaak-plugins",
|
||||
"yaak-sse",
|
||||
@@ -15,7 +18,7 @@ members = [
|
||||
[package]
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
@@ -31,53 +34,49 @@ strip = true # Automatically strip symbols from the binary.
|
||||
cargo-clippy = []
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.5", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.26.0"
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
datetime = "0.5.2"
|
||||
cookie = "0.18.1"
|
||||
encoding_rs = "0.8.35"
|
||||
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
|
||||
hex_color = "3.0.0"
|
||||
http = { version = "1.2.0", default-features = false }
|
||||
log = "0.4.21"
|
||||
log = "0.4.27"
|
||||
md5 = "0.7.0"
|
||||
mime_guess = "2.0.5"
|
||||
rand = "0.9.0"
|
||||
regex = "1.10.2"
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
rustls = { version = "0.23.22", default-features = false, features = ["custom-provider", "ring"] }
|
||||
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-dialog = "2.2.0"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
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.5.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"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
ts-rs = { workspace = true }
|
||||
uuid = "1.12.1"
|
||||
yaak-common = { workspace = true }
|
||||
yaak-crypto = { workspace = true }
|
||||
yaak-git = { path = "yaak-git" }
|
||||
yaak-grpc = { path = "yaak-grpc" }
|
||||
yaak-http = { workspace = true }
|
||||
yaak-license = { path = "yaak-license" }
|
||||
yaak-mac-window = { path = "yaak-mac-window" }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-fonts = { workspace = true }
|
||||
yaak-plugins = { workspace = true }
|
||||
yaak-sse = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
@@ -85,17 +84,24 @@ yaak-templates = { workspace = true }
|
||||
yaak-ws = { path = "yaak-ws" }
|
||||
|
||||
[workspace.dependencies]
|
||||
reqwest = "0.12.12"
|
||||
serde = "1.0.215"
|
||||
serde_json = "1.0.132"
|
||||
tauri = "2.2.5"
|
||||
tauri-plugin = "2.0.4"
|
||||
tauri-plugin-shell = "2.2.0"
|
||||
thiserror = "2.0.3"
|
||||
ts-rs = "10.0.0"
|
||||
reqwest = "0.12.19"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
tauri = "2.5.1"
|
||||
tauri-plugin = "2.2.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tokio = "1.45.1"
|
||||
thiserror = "2.0.12"
|
||||
ts-rs = "11.0.1"
|
||||
rustls = { version = "0.23.27", default-features = false }
|
||||
rustls-platform-verifier = "0.6.0"
|
||||
yaak-common = { path = "yaak-common" }
|
||||
yaak-fonts = { path = "yaak-fonts" }
|
||||
yaak-http = { path = "yaak-http" }
|
||||
yaak-models = { path = "yaak-models" }
|
||||
yaak-plugins = { path = "yaak-plugins" }
|
||||
yaak-sync = { path = "yaak-sync" }
|
||||
yaak-sse = { path = "yaak-sse" }
|
||||
yaak-sync = { path = "yaak-sync" }
|
||||
yaak-templates = { path = "yaak-templates" }
|
||||
yaak-crypto = { path = "yaak-crypto" }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:app:allow-identifier",
|
||||
"core:event:allow-emit",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten",
|
||||
@@ -50,8 +51,12 @@
|
||||
"opener:allow-open-url",
|
||||
"opener:allow-reveal-item-in-dir",
|
||||
"shell:allow-open",
|
||||
"yaak-license:default",
|
||||
"yaak-crypto:default",
|
||||
"yaak-git:default",
|
||||
"yaak-fonts:default",
|
||||
"yaak-license:default",
|
||||
"yaak-mac-window:default",
|
||||
"yaak-models:default",
|
||||
"yaak-sync:default",
|
||||
"yaak-ws:default"
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user