mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 16:01:18 +02:00
Compare commits
162 Commits
v2025.2.2
...
v2025.5.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab02130b0 | ||
|
|
25d50246c0 | ||
|
|
bb0cc16a70 | ||
|
|
8817be679b | ||
|
|
f476d87613 | ||
|
|
1438e8bacc | ||
|
|
7be2767527 | ||
|
|
a1b1eafd39 | ||
|
|
1948fb78bd | ||
|
|
cb7c44cc65 | ||
|
|
b5620fcdf3 | ||
|
|
b8e6dbc7c7 | ||
|
|
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 | ||
|
|
d02883282f | ||
|
|
2c3fb25932 | ||
|
|
4ae7f99264 | ||
|
|
c73f0b02bd | ||
|
|
9615d3e29b | ||
|
|
8c0f889dd2 | ||
|
|
20b0b4fb69 | ||
|
|
8be9c4c388 | ||
|
|
a5333deb71 | ||
|
|
edf65a62c2 | ||
|
|
2cf2c13175 | ||
|
|
493e844c01 | ||
|
|
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 |
@@ -1,6 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.eslintrc.cjs
|
|
||||||
.prettierrc.cjs
|
|
||||||
src-web/postcss.config.cjs
|
|
||||||
src-web/vite.config.ts
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:import/recommended',
|
|
||||||
'plugin:jsx-a11y/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'eslint-config-prettier',
|
|
||||||
],
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
|
||||||
ignorePatterns: [
|
|
||||||
'scripts/**/*',
|
|
||||||
'packages/plugin-runtime/**/*',
|
|
||||||
'packages/plugin-runtime-types/**/*',
|
|
||||||
'src-tauri/**/*',
|
|
||||||
'src-web/tailwind.config.cjs',
|
|
||||||
'src-web/vite.config.ts',
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
node: {
|
|
||||||
paths: ['src-web'],
|
|
||||||
extensions: ['.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': 'error',
|
|
||||||
'jsx-a11y/no-autofocus': 'off',
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'import/no-unresolved': 'off',
|
|
||||||
'@typescript-eslint/consistent-type-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
prefer: 'type-imports',
|
|
||||||
disallowTypeAnnotations: true,
|
|
||||||
fixStyle: 'separate-type-imports',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -3,9 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags: [ v* ]
|
tags: [ v* ]
|
||||||
|
|
||||||
env:
|
|
||||||
YAAK_PLUGINS_DIR: checkout/plugins
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -68,9 +65,7 @@ jobs:
|
|||||||
run: cargo install --force trusted-signing-cli --version 0.5.0
|
run: cargo install --force trusted-signing-cli --version 0.5.0
|
||||||
|
|
||||||
- name: Install NPM Dependencies
|
- name: Install NPM Dependencies
|
||||||
run: |
|
run: npm ci
|
||||||
npm ci
|
|
||||||
npm install @yaakapp/cli
|
|
||||||
|
|
||||||
- name: Install Protoc for plugin-runtime
|
- name: Install Protoc for plugin-runtime
|
||||||
uses: arduino/setup-protoc@v3
|
uses: arduino/setup-protoc@v3
|
||||||
@@ -83,12 +78,6 @@ jobs:
|
|||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: npm 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
|
- name: Set version
|
||||||
run: npm run replace-version
|
run: npm run replace-version
|
||||||
env:
|
env:
|
||||||
@@ -96,7 +85,6 @@ jobs:
|
|||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
|
|
||||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||||
|
|
||||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ Run the `bootstrap` command to do some initial setup:
|
|||||||
npm run bootstrap
|
npm run bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
_NOTE: Run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>` to re-build bundled plugins_
|
|
||||||
|
|
||||||
## Run the App
|
## Run the App
|
||||||
|
|
||||||
After bootstrapping, start the app in development mode:
|
After bootstrapping, start the app in development mode:
|
||||||
@@ -44,22 +42,17 @@ After bootstrapping, start the app in development mode:
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
_NOTE: If working on bundled plugins, run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>`_
|
|
||||||
|
|
||||||
## SQLite Migrations
|
## SQLite Migrations
|
||||||
|
|
||||||
New migrations can be created from the `src-tauri/` directory:
|
New migrations can be created from the `src-tauri/` directory:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd src-tauri
|
npm run migration
|
||||||
sqlx migrate add migration-name
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## Lezer Grammer Generation
|
||||||
|
|
||||||
|
|||||||
88
eslint.config.cjs
Normal file
88
eslint.config.cjs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
const { defineConfig, globalIgnores } = require('eslint/config');
|
||||||
|
|
||||||
|
const { fixupConfigRules } = require('@eslint/compat');
|
||||||
|
|
||||||
|
const reactRefresh = require('eslint-plugin-react-refresh');
|
||||||
|
const tsParser = require('@typescript-eslint/parser');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
|
||||||
|
const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
{
|
||||||
|
extends: fixupConfigRules(
|
||||||
|
compat.extends(
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:jsx-a11y/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'eslint-config-prettier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
paths: ['src-web'],
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'error',
|
||||||
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
prefer: 'type-imports',
|
||||||
|
disallowTypeAnnotations: true,
|
||||||
|
fixStyle: 'separate-type-imports',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalIgnores([
|
||||||
|
'scripts/**/*',
|
||||||
|
'packages/plugin-runtime/**/*',
|
||||||
|
'packages/plugin-runtime-types/**/*',
|
||||||
|
'src-tauri/**/*',
|
||||||
|
'src-web/tailwind.config.cjs',
|
||||||
|
'src-web/vite.config.ts',
|
||||||
|
]),
|
||||||
|
globalIgnores([
|
||||||
|
'**/node_modules/',
|
||||||
|
'**/dist/',
|
||||||
|
'**/.eslintrc.cjs',
|
||||||
|
'**/.prettierrc.cjs',
|
||||||
|
'src-web/postcss.config.cjs',
|
||||||
|
'src-web/vite.config.ts',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
5035
package-lock.json
generated
5035
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -10,8 +10,32 @@
|
|||||||
"packages/plugin-runtime",
|
"packages/plugin-runtime",
|
||||||
"packages/plugin-runtime-types",
|
"packages/plugin-runtime-types",
|
||||||
"packages/common-lib",
|
"packages/common-lib",
|
||||||
|
"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-crypto",
|
||||||
"src-tauri/yaak-git",
|
"src-tauri/yaak-git",
|
||||||
|
"src-tauri/yaak-fonts",
|
||||||
"src-tauri/yaak-license",
|
"src-tauri/yaak-license",
|
||||||
"src-tauri/yaak-mac-window",
|
"src-tauri/yaak-mac-window",
|
||||||
"src-tauri/yaak-models",
|
"src-tauri/yaak-models",
|
||||||
@@ -26,7 +50,9 @@
|
|||||||
"start": "npm run app-dev",
|
"start": "npm run app-dev",
|
||||||
"app-build": "tauri build",
|
"app-build": "tauri build",
|
||||||
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
"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": "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": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
|
||||||
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
|
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
|
||||||
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
||||||
@@ -35,24 +61,29 @@
|
|||||||
"replace-version": "node scripts/replace-version.cjs",
|
"replace-version": "node scripts/replace-version.cjs",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
|
"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": {
|
"dependencies": {
|
||||||
"jotai": "^2.12.2"
|
"jotai": "^2.12.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.4.1",
|
"@eslint/compat": "^1.3.0",
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.29.0",
|
||||||
|
"@tauri-apps/cli": "2.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
||||||
"@typescript-eslint/parser": "^8.27.0",
|
"@typescript-eslint/parser": "^8.27.0",
|
||||||
"eslint": "^8",
|
"@yaakapp/cli": "^0.1.5",
|
||||||
"eslint-config-prettier": "^8",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.8.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",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.5.3",
|
"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",
|
"main": "lib/index.js",
|
||||||
"typings": "./lib/index.d.ts",
|
"typings": "./lib/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
8
packages/plugin-runtime-types/src/bindings/gen_api.ts
Normal file
8
packages/plugin-runtime-types/src/bindings/gen_api.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { PluginVersion } from "./gen_search.js";
|
||||||
|
|
||||||
|
export type PluginNameVersion = { name: string, version: string, };
|
||||||
|
|
||||||
|
export type PluginSearchResponse = { plugins: Array<PluginVersion>, };
|
||||||
|
|
||||||
|
export type PluginUpdatesResponse = { plugins: Array<PluginNameVersion>, };
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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 { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } 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 { JsonValue } from "./serde_json/JsonValue.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, };
|
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 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, };
|
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||||
|
|
||||||
@@ -104,7 +98,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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 = {
|
export type FormInputCheckbox = {
|
||||||
/**
|
/**
|
||||||
@@ -131,7 +129,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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 = {
|
export type FormInputEditor = {
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +172,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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 = {
|
export type FormInputFile = {
|
||||||
/**
|
/**
|
||||||
@@ -205,7 +211,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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 = {
|
export type FormInputHttpRequest = {
|
||||||
/**
|
/**
|
||||||
@@ -232,7 +242,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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, };
|
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||||
|
|
||||||
@@ -265,7 +279,11 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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, };
|
export type FormInputSelectOption = { label: string, value: string, };
|
||||||
|
|
||||||
@@ -306,10 +324,18 @@ hideLabel?: boolean,
|
|||||||
/**
|
/**
|
||||||
* The default value
|
* 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 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 GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
|
||||||
|
|
||||||
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
|
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
|
||||||
@@ -346,10 +372,14 @@ export type ImportResponse = { resources: ImportResources, };
|
|||||||
|
|
||||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, 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" } & BootResponse | { "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 JsonPrimitive = string | number | boolean | null;
|
||||||
|
|
||||||
|
export type ListCookieNamesRequest = {};
|
||||||
|
|
||||||
|
export type ListCookieNamesResponse = { names: Array<string>, };
|
||||||
|
|
||||||
export type OpenWindowRequest = { url: string,
|
export type OpenWindowRequest = { url: string,
|
||||||
/**
|
/**
|
||||||
* Label for the window. If not provided, a random one will be generated.
|
* Label for the window. If not provided, a random one will be generated.
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, 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 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<HttpRequestHeader>, 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<GrpcMetadataEntry>, 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>, };
|
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 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, encryptionKeyChallenge: string | null, 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, };
|
||||||
|
|||||||
5
packages/plugin-runtime-types/src/bindings/gen_search.ts
Normal file
5
packages/plugin-runtime-types/src/bindings/gen_search.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, };
|
||||||
|
|
||||||
|
export type PluginVersion = { id: string, version: string, url: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, };
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import type {
|
import type {
|
||||||
FindHttpResponsesRequest,
|
FindHttpResponsesRequest,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
|
GetCookieValueRequest,
|
||||||
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdRequest,
|
GetHttpRequestByIdRequest,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
|
ListCookieNamesResponse,
|
||||||
OpenWindowRequest,
|
OpenWindowRequest,
|
||||||
PromptTextRequest,
|
PromptTextRequest,
|
||||||
PromptTextResponse,
|
PromptTextResponse,
|
||||||
@@ -38,6 +41,10 @@ export interface Context {
|
|||||||
},
|
},
|
||||||
): Promise<{ close: () => void }>;
|
): Promise<{ close: () => void }>;
|
||||||
};
|
};
|
||||||
|
cookies: {
|
||||||
|
listNames(): Promise<ListCookieNamesResponse['names']>;
|
||||||
|
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
|
||||||
|
};
|
||||||
httpRequest: {
|
httpRequest: {
|
||||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
||||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
import {
|
||||||
import type {
|
|
||||||
BootRequest,
|
BootRequest,
|
||||||
Context,
|
BootResponse,
|
||||||
DeleteKeyValueResponse,
|
DeleteKeyValueResponse,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
FormInput,
|
FormInput,
|
||||||
|
GetCookieValueRequest,
|
||||||
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
GetKeyValueResponse,
|
GetKeyValueResponse,
|
||||||
HttpAuthenticationAction,
|
HttpAuthenticationAction,
|
||||||
HttpRequestAction,
|
HttpRequestAction,
|
||||||
InternalEvent,
|
InternalEvent,
|
||||||
InternalEventPayload,
|
InternalEventPayload,
|
||||||
JsonPrimitive,
|
ListCookieNamesResponse,
|
||||||
PluginDefinition,
|
PluginWindowContext,
|
||||||
PromptTextResponse,
|
PromptTextResponse,
|
||||||
RenderHttpRequestResponse,
|
RenderHttpRequestResponse,
|
||||||
SendHttpRequestResponse,
|
SendHttpRequestResponse,
|
||||||
TemplateFunction,
|
TemplateFunction,
|
||||||
|
TemplateFunctionArg,
|
||||||
TemplateRenderResponse,
|
TemplateRenderResponse,
|
||||||
} 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 console from 'node:console';
|
||||||
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
|
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
// import util from 'node:util';
|
|
||||||
import { EventChannel } from './EventChannel';
|
import { EventChannel } from './EventChannel';
|
||||||
// import { interceptStdout } from './interceptStdout';
|
|
||||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||||
|
|
||||||
export interface PluginWorkerData {
|
export interface PluginWorkerData {
|
||||||
@@ -51,9 +53,22 @@ export class PluginInstance {
|
|||||||
|
|
||||||
// Reload plugin if the JS or package.json changes
|
// Reload plugin if the JS or package.json changes
|
||||||
const windowContextNone: PluginWindowContext = { type: 'none' };
|
const windowContextNone: PluginWindowContext = { type: 'none' };
|
||||||
|
|
||||||
|
this.#mod = {};
|
||||||
|
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
|
||||||
|
|
||||||
|
const bootResponse: BootResponse = {
|
||||||
|
name: this.#pkg.name ?? 'unknown',
|
||||||
|
version: this.#pkg.version ?? '0.0.1',
|
||||||
|
};
|
||||||
|
|
||||||
const fileChangeCallback = async () => {
|
const fileChangeCallback = async () => {
|
||||||
this.#importModule();
|
this.#importModule();
|
||||||
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
return this.#sendPayload(
|
||||||
|
windowContextNone,
|
||||||
|
{ type: 'reload_response', ...bootResponse },
|
||||||
|
null,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.#workerData.bootRequest.watch) {
|
if (this.#workerData.bootRequest.watch) {
|
||||||
@@ -61,12 +76,6 @@ export class PluginInstance {
|
|||||||
watchFile(this.#pathPkg(), fileChangeCallback);
|
watchFile(this.#pathPkg(), fileChangeCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#mod = {};
|
|
||||||
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
|
|
||||||
|
|
||||||
// TODO: Re-implement this now that we're not using workers
|
|
||||||
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
|
|
||||||
|
|
||||||
this.#importModule();
|
this.#importModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,15 +290,9 @@ export class PluginInstance {
|
|||||||
this.#importModule();
|
this.#importModule();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Plugin call threw exception', payload.type, err);
|
const error = `${err}`.replace(/^Error:\s*/g, '');
|
||||||
this.#sendPayload(
|
console.log('Plugin call threw exception', payload.type, '→', error);
|
||||||
windowContext,
|
this.#sendPayload(windowContext, { type: 'error_response', error }, replyId);
|
||||||
{
|
|
||||||
type: 'error_response',
|
|
||||||
error: `${err}`,
|
|
||||||
},
|
|
||||||
replyId,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,6 +498,27 @@ export class PluginInstance {
|
|||||||
return httpRequest;
|
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: {
|
templates: {
|
||||||
/**
|
/**
|
||||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||||
@@ -552,7 +576,7 @@ function genId(len = 5): string {
|
|||||||
/** Recursively apply form input defaults to a set of values */
|
/** Recursively apply form input defaults to a set of values */
|
||||||
function applyFormInputDefaults(
|
function applyFormInputDefaults(
|
||||||
inputs: TemplateFunctionArg[],
|
inputs: TemplateFunctionArg[],
|
||||||
values: { [p: string]: JsonPrimitive | undefined },
|
values: { [p: string]: JsonValue | undefined },
|
||||||
) {
|
) {
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
if ('inputs' in input) {
|
if ('inputs' in input) {
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ async function handleIncoming(msg: string) {
|
|||||||
|
|
||||||
plugin.sendToWorker(pluginEvent);
|
plugin.sendToWorker(pluginEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
|
|||||||
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/fetchAccessToken.ts
Normal file
78
plugins/auth-oauth2/src/fetchAccessToken.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import type { AccessTokenRawResponse } from './store';
|
||||||
|
|
||||||
|
export async function fetchAccessToken(
|
||||||
|
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;
|
||||||
|
}
|
||||||
19
plugins/auth-oauth2/src/getAccessTokenIfNotExpired.ts
Normal file
19
plugins/auth-oauth2/src/getAccessTokenIfNotExpired.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Context } from '@yaakapp/api';
|
||||||
|
import type { AccessToken } from './store';
|
||||||
|
import { getToken } from './store';
|
||||||
|
|
||||||
|
export async function getAccessTokenIfNotExpired(
|
||||||
|
ctx: Context,
|
||||||
|
contextId: string,
|
||||||
|
): Promise<AccessToken | null> {
|
||||||
|
const token = await getToken(ctx, contextId);
|
||||||
|
if (token == null || isTokenExpired(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTokenExpired(token: AccessToken) {
|
||||||
|
return token.expiresAt && Date.now() > token.expiresAt;
|
||||||
|
}
|
||||||
112
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
112
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import type { Context, HttpRequest } from '@yaakapp/api';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { isTokenExpired } from './getAccessTokenIfNotExpired';
|
||||||
|
import type { AccessToken, AccessTokenRawResponse } from './store';
|
||||||
|
import { 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 isExpired = isTokenExpired(token);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
151
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
151
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import type { Context } from '@yaakapp/api';
|
||||||
|
import { createHash, randomBytes } from 'node:crypto';
|
||||||
|
import { fetchAccessToken } from '../fetchAccessToken';
|
||||||
|
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||||
|
import type { AccessToken } from '../store';
|
||||||
|
import { 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;
|
||||||
|
codeVerifier: string;
|
||||||
|
} | 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) {
|
||||||
|
authorizationUrl.searchParams.set(
|
||||||
|
'code_challenge',
|
||||||
|
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
|
||||||
|
);
|
||||||
|
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
|
||||||
|
const dataDirKey = await getDataDirKey(ctx, contextId);
|
||||||
|
const authorizationUrlStr = authorizationUrl.toString();
|
||||||
|
console.log('[oauth2] Authorizing', authorizationUrlStr);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
const code = await new Promise<string>(async (resolve, reject) => {
|
||||||
|
let foundCode = false;
|
||||||
|
const { close } = await ctx.window.openUrl({
|
||||||
|
url: authorizationUrlStr,
|
||||||
|
label: 'oauth-authorization-url',
|
||||||
|
dataDirKey,
|
||||||
|
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')) {
|
||||||
|
close();
|
||||||
|
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();
|
||||||
|
resolve(code);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[oauth2] Code found');
|
||||||
|
const response = await fetchAccessToken(ctx, {
|
||||||
|
grantType: 'authorization_code',
|
||||||
|
accessTokenUrl,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
scope,
|
||||||
|
audience,
|
||||||
|
credentialsInBody,
|
||||||
|
params: [
|
||||||
|
{ name: 'code', value: code },
|
||||||
|
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
|
||||||
|
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeToken(ctx, contextId, response, tokenName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genPkceCodeVerifier() {
|
||||||
|
return encodeForPkce(randomBytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pkceCodeChallenge(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 '_'
|
||||||
|
}
|
||||||
42
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
42
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Context } from '@yaakapp/api';
|
||||||
|
import { fetchAccessToken } from '../fetchAccessToken';
|
||||||
|
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
|
||||||
|
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 && !isTokenExpired(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchAccessToken(ctx, {
|
||||||
|
grantType: 'client_credentials',
|
||||||
|
accessTokenUrl,
|
||||||
|
audience,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
scope,
|
||||||
|
credentialsInBody,
|
||||||
|
params: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeToken(ctx, contextId, response);
|
||||||
|
}
|
||||||
89
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
89
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Context } from '@yaakapp/api';
|
||||||
|
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
|
||||||
|
import type { AccessToken, AccessTokenRawResponse} from '../store';
|
||||||
|
import { getToken, storeToken } from '../store';
|
||||||
|
|
||||||
|
export async 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> {
|
||||||
|
const token = await getToken(ctx, contextId);
|
||||||
|
if (token != null && !isTokenExpired(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
|
||||||
|
let foundAccessToken = false;
|
||||||
|
const authorizationUrlStr = authorizationUrl.toString();
|
||||||
|
const { 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(storeToken(ctx, contextId, response));
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
56
plugins/auth-oauth2/src/grants/password.ts
Normal file
56
plugins/auth-oauth2/src/grants/password.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Context } from '@yaakapp/api';
|
||||||
|
import { fetchAccessToken } from '../fetchAccessToken';
|
||||||
|
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||||
|
import type { AccessToken} from '../store';
|
||||||
|
import { 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 fetchAccessToken(ctx, {
|
||||||
|
accessTokenUrl,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
scope,
|
||||||
|
audience,
|
||||||
|
grantType: 'password',
|
||||||
|
credentialsInBody,
|
||||||
|
params: [
|
||||||
|
{ name: 'username', value: username },
|
||||||
|
{ name: 'password', value: password },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeToken(ctx, contextId, response);
|
||||||
|
}
|
||||||
406
plugins/auth-oauth2/src/index.ts
Normal file
406
plugins/auth-oauth2/src/index.ts
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import type {
|
||||||
|
Context,
|
||||||
|
FormInputSelectOption,
|
||||||
|
GetHttpAuthenticationConfigRequest,
|
||||||
|
JsonPrimitive,
|
||||||
|
PluginDefinition,
|
||||||
|
} from '@yaakapp/api';
|
||||||
|
import {
|
||||||
|
genPkceCodeVerifier,
|
||||||
|
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 type { AccessToken } from './store';
|
||||||
|
import { 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: 'pkceCodeChallenge',
|
||||||
|
label: 'Code Verifier',
|
||||||
|
placeholder: 'Automatically generated when not set',
|
||||||
|
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') || DEFAULT_PKCE_METHOD,
|
||||||
|
codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(),
|
||||||
|
}
|
||||||
|
: 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"
|
"dev": "yaakcli dev ./src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^9.0.0"
|
"jsonpath-plus": "^10.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonpath": "^0.2.4"
|
"@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
9
plugins/template-function-regex/package.json
Normal file
9
plugins/template-function-regex/package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaakapp/template-function-regex",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaakcli build ./src/index.ts",
|
||||||
|
"dev": "yaakcli dev ./src/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
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"
|
"dev": "yaakcli dev ./src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^9.0.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
"xpath": "^0.0.34",
|
"xpath": "^0.0.34",
|
||||||
"@xmldom/xmldom": "^0.8.10"
|
"@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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
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);
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
const { readdirSync, cpSync } = require('node:fs');
|
const { readdirSync, cpSync } = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { execSync } = require('node:child_process');
|
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);
|
const pluginsDir = path.join(__dirname, '..', 'plugins');
|
||||||
execSync('npm ci', { cwd: pluginsDir });
|
|
||||||
console.log('Building Yaak plugins', pluginsDir);
|
|
||||||
execSync('npm run build', { cwd: pluginsDir });
|
|
||||||
|
|
||||||
console.log('Copying Yaak plugins to', pluginsDir);
|
console.log('Copying Yaak plugins to', pluginsDir);
|
||||||
|
|
||||||
const pluginsRoot = path.join(pluginsDir, 'plugins');
|
for (const name of readdirSync(pluginsDir)) {
|
||||||
for (const name of readdirSync(pluginsRoot)) {
|
const dir = path.join(pluginsDir, name);
|
||||||
const dir = path.join(pluginsRoot, name);
|
|
||||||
if (name.startsWith('.')) continue;
|
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);
|
const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name);
|
||||||
console.log(`Copying ${name} to ${destDir}`);
|
console.log(`Copying ${name} to ${destDir}`);
|
||||||
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
||||||
|
|||||||
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@@ -3,7 +3,6 @@
|
|||||||
target/
|
target/
|
||||||
|
|
||||||
vendored/*
|
vendored/*
|
||||||
!vendored/plugins
|
|
||||||
|
|
||||||
gen/*
|
gen/*
|
||||||
|
|
||||||
|
|||||||
3500
src-tauri/Cargo.lock
generated
3500
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
|
"yaak-fonts",
|
||||||
"yaak-git",
|
"yaak-git",
|
||||||
"yaak-grpc",
|
"yaak-grpc",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
@@ -33,13 +34,14 @@ strip = true # Automatically strip symbols from the binary.
|
|||||||
cargo-clippy = []
|
cargo-clippy = []
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.1.1", features = [] }
|
tauri-build = { version = "2.2.0", features = [] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
|
cookie = "0.18.1"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
|
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
|
||||||
http = { version = "1.2.0", default-features = false }
|
http = { version = "1.2.0", default-features = false }
|
||||||
@@ -47,29 +49,29 @@ log = "0.4.27"
|
|||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
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"
|
reqwest_cookie_store = "0.8.0"
|
||||||
rustls = { version = "0.23.25", default-features = false, features = ["custom-provider", "ring"] }
|
|
||||||
rustls-platform-verifier = "0.5.1"
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||||
tauri-plugin-clipboard-manager = "2.2.2"
|
tauri-plugin-clipboard-manager = "2.2.2"
|
||||||
tauri-plugin-dialog = "2.2.0"
|
tauri-plugin-dialog = { workspace = true }
|
||||||
tauri-plugin-fs = "2.2.0"
|
tauri-plugin-fs = "2.3.0"
|
||||||
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
|
tauri-plugin-log = { version = "2.4.0", features = ["colored"] }
|
||||||
tauri-plugin-opener = "2.2.6"
|
tauri-plugin-opener = "2.2.6"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
tauri-plugin-shell = { workspace = true }
|
tauri-plugin-shell = { workspace = true }
|
||||||
tauri-plugin-single-instance = "2.2.2"
|
tauri-plugin-deep-link = "2.3.0"
|
||||||
tauri-plugin-updater = "2.6.1"
|
tauri-plugin-single-instance = { version = "2.2.4", features = ["deep-link"] }
|
||||||
tauri-plugin-window-state = "2.2.1"
|
tauri-plugin-updater = "2.7.1"
|
||||||
|
tauri-plugin-window-state = "2.2.2"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync"] }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
uuid = "1.12.1"
|
uuid = "1.12.1"
|
||||||
yaak-common = { workspace = true }
|
yaak-common = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
|
yaak-fonts = { workspace = true }
|
||||||
yaak-git = { path = "yaak-git" }
|
yaak-git = { path = "yaak-git" }
|
||||||
yaak-grpc = { path = "yaak-grpc" }
|
yaak-grpc = { path = "yaak-grpc" }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
@@ -83,20 +85,27 @@ yaak-templates = { workspace = true }
|
|||||||
yaak-ws = { path = "yaak-ws" }
|
yaak-ws = { path = "yaak-ws" }
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
reqwest = "0.12.15"
|
chrono = "0.4.41"
|
||||||
|
hex = "0.4.3"
|
||||||
|
reqwest = "0.12.20"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
tauri = "2.4.1"
|
tauri = "2.5.1"
|
||||||
tauri-plugin = "2.1.1"
|
tauri-plugin = "2.2.0"
|
||||||
|
tauri-plugin-dialog = "2.2.2"
|
||||||
tauri-plugin-shell = "2.2.1"
|
tauri-plugin-shell = "2.2.1"
|
||||||
tokio = "1.44.2"
|
tokio = "1.45.1"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
ts-rs = "10.1.0"
|
ts-rs = "11.0.1"
|
||||||
|
rustls = { version = "0.23.27", default-features = false }
|
||||||
|
rustls-platform-verifier = "0.6.0"
|
||||||
|
sha2 = "0.10.9"
|
||||||
yaak-common = { path = "yaak-common" }
|
yaak-common = { path = "yaak-common" }
|
||||||
|
yaak-crypto = { path = "yaak-crypto" }
|
||||||
|
yaak-fonts = { path = "yaak-fonts" }
|
||||||
yaak-http = { path = "yaak-http" }
|
yaak-http = { path = "yaak-http" }
|
||||||
yaak-models = { path = "yaak-models" }
|
yaak-models = { path = "yaak-models" }
|
||||||
yaak-plugins = { path = "yaak-plugins" }
|
yaak-plugins = { path = "yaak-plugins" }
|
||||||
yaak-sse = { path = "yaak-sse" }
|
yaak-sse = { path = "yaak-sse" }
|
||||||
yaak-sync = { path = "yaak-sync" }
|
yaak-sync = { path = "yaak-sync" }
|
||||||
yaak-templates = { path = "yaak-templates" }
|
yaak-templates = { path = "yaak-templates" }
|
||||||
yaak-crypto = { path = "yaak-crypto" }
|
|
||||||
|
|||||||
@@ -52,10 +52,12 @@
|
|||||||
"opener:allow-reveal-item-in-dir",
|
"opener:allow-reveal-item-in-dir",
|
||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"yaak-crypto:default",
|
"yaak-crypto:default",
|
||||||
|
"yaak-fonts:default",
|
||||||
"yaak-git:default",
|
"yaak-git:default",
|
||||||
"yaak-license:default",
|
"yaak-license:default",
|
||||||
"yaak-mac-window:default",
|
"yaak-mac-window:default",
|
||||||
"yaak-models:default",
|
"yaak-models:default",
|
||||||
|
"yaak-plugins:default",
|
||||||
"yaak-sync:default",
|
"yaak-sync:default",
|
||||||
"yaak-ws:default"
|
"yaak-ws:default"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<!-- Enable for v8 execution -->
|
<!-- Enable for NodeJS execution -->
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SyncError(#[from] yaak_sync::error::Error),
|
SyncError(#[from] yaak_sync::error::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
CryptoError(#[from] yaak_crypto::error::Error),
|
CryptoError(#[from] yaak_crypto::error::Error),
|
||||||
|
|
||||||
@@ -28,18 +28,21 @@ pub enum Error {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PluginError(#[from] yaak_plugins::error::Error),
|
PluginError(#[from] yaak_plugins::error::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
CommonError(#[from] yaak_common::error::Error),
|
||||||
|
|
||||||
#[error("Updater error: {0}")]
|
#[error("Updater error: {0}")]
|
||||||
UpdaterError(#[from] tauri_plugin_updater::Error),
|
UpdaterError(#[from] tauri_plugin_updater::Error),
|
||||||
|
|
||||||
#[error("JSON error: {0}")]
|
#[error("JSON error: {0}")]
|
||||||
JsonError(#[from] serde_json::error::Error),
|
JsonError(#[from] serde_json::error::Error),
|
||||||
|
|
||||||
#[error("Tauri error: {0}")]
|
#[error("Tauri error: {0}")]
|
||||||
TauriError(#[from] tauri::Error),
|
TauriError(#[from] tauri::Error),
|
||||||
|
|
||||||
#[error("Event source error: {0}")]
|
#[error("Event source error: {0}")]
|
||||||
EventSourceError(#[from] eventsource_client::Error),
|
EventSourceError(#[from] eventsource_client::Error),
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
IOError(#[from] io::Error),
|
IOError(#[from] io::Error),
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use KeyAndValueRef::{Ascii, Binary};
|
|||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||||
use yaak_models::models::GrpcRequest;
|
use yaak_models::models::GrpcRequest;
|
||||||
|
use yaak_models::query_manager::QueryManagerExt;
|
||||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
@@ -19,15 +20,33 @@ pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String>
|
|||||||
entries
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_grpc_request<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
request: &GrpcRequest,
|
||||||
|
) -> Result<(GrpcRequest, String)> {
|
||||||
|
let mut new_request = request.clone();
|
||||||
|
|
||||||
|
let (authentication_type, authentication, authentication_context_id) =
|
||||||
|
window.db().resolve_auth_for_grpc_request(request)?;
|
||||||
|
new_request.authentication_type = authentication_type;
|
||||||
|
new_request.authentication = authentication;
|
||||||
|
|
||||||
|
let metadata = window.db().resolve_metadata_for_grpc_request(request)?;
|
||||||
|
new_request.metadata = metadata;
|
||||||
|
|
||||||
|
Ok((new_request, authentication_context_id))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn build_metadata<R: Runtime>(
|
pub(crate) async fn build_metadata<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
request: &GrpcRequest,
|
request: &GrpcRequest,
|
||||||
|
authentication_context_id: &str,
|
||||||
) -> Result<BTreeMap<String, String>> {
|
) -> Result<BTreeMap<String, String>> {
|
||||||
let plugin_manager = window.state::<PluginManager>();
|
let plugin_manager = window.state::<PluginManager>();
|
||||||
let mut metadata = BTreeMap::new();
|
let mut metadata = BTreeMap::new();
|
||||||
|
|
||||||
// Add the rest of metadata
|
// Add the rest of metadata
|
||||||
for h in request.clone().metadata {
|
for h in request.metadata.clone() {
|
||||||
if h.name.is_empty() && h.value.is_empty() {
|
if h.name.is_empty() && h.value.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -39,25 +58,34 @@ pub(crate) async fn build_metadata<R: Runtime>(
|
|||||||
metadata.insert(h.name, h.value);
|
metadata.insert(h.name, h.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(auth_name) = request.authentication_type.clone() {
|
match request.authentication_type.clone() {
|
||||||
let auth = request.authentication.clone();
|
None => {
|
||||||
let plugin_req = CallHttpAuthenticationRequest {
|
// No authentication found. Not even inherited
|
||||||
context_id: format!("{:x}", md5::compute(request.id.clone())),
|
}
|
||||||
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
Some(authentication_type) if authentication_type == "none" => {
|
||||||
method: "POST".to_string(),
|
// Explicitly no authentication
|
||||||
url: request.url.clone(),
|
}
|
||||||
headers: metadata
|
Some(authentication_type) => {
|
||||||
.iter()
|
let auth = request.authentication.clone();
|
||||||
.map(|(name, value)| HttpHeader {
|
let plugin_req = CallHttpAuthenticationRequest {
|
||||||
name: name.to_string(),
|
context_id: format!("{:x}", md5::compute(authentication_context_id)),
|
||||||
value: value.to_string(),
|
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
|
||||||
})
|
method: "POST".to_string(),
|
||||||
.collect(),
|
url: request.url.clone(),
|
||||||
};
|
headers: metadata
|
||||||
let plugin_result =
|
.iter()
|
||||||
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
|
.map(|(name, value)| HttpHeader {
|
||||||
for header in plugin_result.set_headers {
|
name: name.to_string(),
|
||||||
metadata.insert(header.name, header.value);
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let plugin_result = plugin_manager
|
||||||
|
.call_http_authentication(&window, &authentication_type, plugin_req)
|
||||||
|
.await?;
|
||||||
|
for header in plugin_result.set_headers {
|
||||||
|
metadata.insert(header.name, header.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user