mirror of
https://github.com/mountain-loop/yaak.git
synced 2025-12-05 19:17:44 -06:00
Merge plugins repo into mono
This commit is contained in:
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -3,9 +3,6 @@ on:
|
||||
push:
|
||||
tags: [ v* ]
|
||||
|
||||
env:
|
||||
YAAK_PLUGINS_DIR: checkout/plugins
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
permissions:
|
||||
@@ -83,12 +80,6 @@ jobs:
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Checkout yaakapp/plugins
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaakapp/plugins
|
||||
path: ${{ env.YAAK_PLUGINS_DIR }}
|
||||
|
||||
- name: Set version
|
||||
run: npm run replace-version
|
||||
env:
|
||||
@@ -96,7 +87,6 @@ jobs:
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
|
||||
121
.idea/codeStyles/Project.xml
generated
Normal file
121
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,121 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<GoCodeStyleSettings>
|
||||
<option name="ADD_PARENTHESES_FOR_SINGLE_IMPORT" value="true" />
|
||||
<option name="LOCAL_PACKAGE_PREFIXES" />
|
||||
</GoCodeStyleSettings>
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<Markdown>
|
||||
<option name="KEEP_LINE_BREAKS_INSIDE_TEXT_BLOCKS" value="false" />
|
||||
</Markdown>
|
||||
<PostCssCodeStyleSettings>
|
||||
<option name="COMMENTS_INLINE_STYLE" value="false" />
|
||||
</PostCssCodeStyleSettings>
|
||||
<SqlCodeStyleSettings version="7">
|
||||
<option name="USE_GENERAL_STYLE" value="true" />
|
||||
<option name="KEYWORD_CASE" value="2" />
|
||||
<option name="IDENTIFIER_CASE" value="1" />
|
||||
<option name="TYPE_CASE" value="2" />
|
||||
<option name="CUSTOM_TYPE_CASE" value="2" />
|
||||
<option name="BUILT_IN_CASE" value="2" />
|
||||
<option name="SUBQUERY_OPENING" value="1" />
|
||||
<option name="SUBQUERY_CONTENT" value="1" />
|
||||
<option name="SUBQUERY_CLOSING" value="1" />
|
||||
<option name="SUBQUERY_PAR_SPACE_INSIDE" value="true" />
|
||||
<option name="EXPR_CASE_WHEN_WRAP" value="false" />
|
||||
<option name="EXPR_CASE_END" value="9" />
|
||||
<option name="ALIGN_AS_IN_SELECT_STATEMENT" value="false" />
|
||||
<option name="ALIGN_TYPE_IN_CREATE_STATEMENT" value="false" />
|
||||
<option name="ALIGN_TYPE_IN_BLOCK_STATEMENT" value="false" />
|
||||
<option name="ALIGN_TYPE_IN_ARGUMENT_DEFINITION" value="false" />
|
||||
<option name="ALIGN_INSIDE_BINARY_EXPRESSION" value="false" />
|
||||
<option name="ALIGN_INSIDE_QUERY_EXPRESSION" value="false" />
|
||||
<option name="ALIGN_EQ_INSIDE_SET_CLAUSE" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_FROM" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_JOIN" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_WHERE" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_GROUP_BY" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_ORDER_BY" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_HAVING" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_THEN" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_ELSE" value="false" />
|
||||
<option name="NEW_LINE_BEFORE_OTHER_CLAUSES" value="false" />
|
||||
<option name="INDENT_JOIN" value="false" />
|
||||
<option name="INDENT_JOIN_CONDITION" value="false" />
|
||||
<option name="WRAP_INSIDE_CREATE_TABLE" value="0" />
|
||||
<option name="WRAP_INSIDE_SELECT" value="0" />
|
||||
<option name="WRAP_INSIDE_JOIN_EXPRESSION" value="0" />
|
||||
<option name="WRAP_INSIDE_GROUP_BY" value="0" />
|
||||
<option name="WRAP_INSIDE_WHERE" value="0" />
|
||||
<option name="WRAP_INSIDE_ORDER_BY" value="0" />
|
||||
<option name="WRAP_INSIDE_SET" value="0" />
|
||||
<option name="WRAP_INSIDE_ARGUMENT_DEFINITION" value="0" />
|
||||
<option name="WRAP_INSIDE_CALL_EXPRESSION" value="0" />
|
||||
<option name="WRAP_INSIDE_VALUES_EXPRESSION" value="0" />
|
||||
<option name="WRAP_VALUES_EXPRESSION" value="0" />
|
||||
<option name="WRAP_PARENTHESIZED_EXPRESSION_INSIDE_VALUES" value="0" />
|
||||
<option name="NEW_LINE_AFTER_SELECT_ITEM" value="false" />
|
||||
</SqlCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
<option name="IMPORT_SORT_MODULE_NAME" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Markdown">
|
||||
<option name="RIGHT_MARGIN" value="90" />
|
||||
<option name="WRAP_ON_TYPING" value="0" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="SQL">
|
||||
<indentOptions>
|
||||
<option name="TAB_SIZE" value="8" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PointlessBooleanExpressionJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$" libraries="{Node.js Core}" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/plugins.iml" filepath="$PROJECT_DIR$/.idea/plugins.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
50
.idea/plugins.iml
generated
Normal file
50
.idea/plugins.iml
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-common/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-crypto/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-git/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-grpc/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-http/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-license/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-mac-window/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-models/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-plugins/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-sse/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-sync/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-templates/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src-tauri/yaak-ws/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/exporter-curl/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/filter-jsonpath/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/filter-xpath/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/importer-curl/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/importer-insomnia/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/importer-openapi/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/importer-postman/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/importer-yaak/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/template-function-fs/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/template-function-hash/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/template-function-json/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/template-function-request/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/plugins_/template-function-response/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-crypto/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-git/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-grpc/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-http/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-license/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-mac-window/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-models/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-plugins/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-sync/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-templates/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src-tauri/yaak-ws/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1026
.idea/workspace.xml
generated
Normal file
1026
.idea/workspace.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,6 @@ Run the `bootstrap` command to do some initial setup:
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
_NOTE: Run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>` to re-build bundled plugins_
|
||||
|
||||
## Run the App
|
||||
|
||||
After bootstrapping, start the app in development mode:
|
||||
@@ -44,8 +42,6 @@ After bootstrapping, start the app in development mode:
|
||||
npm start
|
||||
```
|
||||
|
||||
_NOTE: If working on bundled plugins, run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>`_
|
||||
|
||||
## SQLite Migrations
|
||||
|
||||
New migrations can be created from the `src-tauri/` directory:
|
||||
|
||||
3608
package-lock.json
generated
3608
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -10,6 +10,29 @@
|
||||
"packages/plugin-runtime",
|
||||
"packages/plugin-runtime-types",
|
||||
"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-git",
|
||||
"src-tauri/yaak-license",
|
||||
@@ -27,6 +50,7 @@
|
||||
"app-build": "tauri build",
|
||||
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
"build": "npm run --workspaces --if-present build",
|
||||
"build-plugins": "npm run --workspaces --if-present build",
|
||||
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
|
||||
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
|
||||
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
||||
@@ -35,7 +59,7 @@
|
||||
"replace-version": "node scripts/replace-version.cjs",
|
||||
"tauri": "tauri",
|
||||
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
|
||||
"tauri-before-dev": "npm run --workspaces --if-present dev"
|
||||
"tauri-before-dev": "workspaces-run --parallel -- npm run --workspaces --if-present dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"jotai": "^2.12.2"
|
||||
@@ -53,6 +77,7 @@
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.2",
|
||||
"workspaces-run": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
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 }] };
|
||||
}
|
||||
,
|
||||
},
|
||||
}
|
||||
;
|
||||
74
plugins/auth-oauth2/src/getAccessToken.ts
Normal file
74
plugins/auth-oauth2/src/getAccessToken.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { AccessTokenRawResponse } from './store';
|
||||
|
||||
export async function getAccessToken(
|
||||
ctx: Context, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
audience,
|
||||
params,
|
||||
grantType,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
}: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
grantType: string;
|
||||
accessTokenUrl: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
params: HttpUrlParameter[];
|
||||
}): Promise<AccessTokenRawResponse> {
|
||||
console.log('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 });
|
||||
}
|
||||
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
|
||||
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;
|
||||
}
|
||||
99
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
99
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Context, HttpRequest } from '@yaakapp/api';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store';
|
||||
|
||||
export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
|
||||
scope,
|
||||
accessTokenUrl,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
forceRefresh,
|
||||
}: {
|
||||
scope: string | null;
|
||||
accessTokenUrl: string;
|
||||
credentialsInBody: boolean;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
forceRefresh?: boolean;
|
||||
}): Promise<AccessToken | null> {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const isExpired = token.expiresAt && now > token.expiresAt;
|
||||
|
||||
// Return the current access token if it's still valid
|
||||
if (!isExpired && !forceRefresh) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token is expired, but there's no refresh token :(
|
||||
if (!token.response.refresh_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Access token is expired, so get a new one
|
||||
const httpRequest: Partial<HttpRequest> = {
|
||||
method: 'POST',
|
||||
url: accessTokenUrl,
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'grant_type', value: 'refresh_token' },
|
||||
{ name: 'refresh_token', value: token.response.refresh_token },
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{ name: 'User-Agent', value: 'yaak' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
],
|
||||
};
|
||||
|
||||
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
|
||||
|
||||
if (credentialsInBody) {
|
||||
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
|
||||
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
|
||||
} else {
|
||||
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
||||
httpRequest.headers!.push({ name: 'Authorization', value });
|
||||
}
|
||||
|
||||
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('Unauthorized refresh_token request');
|
||||
await deleteToken(ctx, contextId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
|
||||
|
||||
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);
|
||||
}
|
||||
140
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
140
plugins/auth-oauth2/src/grants/authorizationCode.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import { AccessToken, getDataDirKey, storeToken } from '../store';
|
||||
|
||||
export const PKCE_SHA256 = 'S256';
|
||||
export const PKCE_PLAIN = 'plain';
|
||||
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
||||
|
||||
export async function getAuthorizationCode(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
pkce,
|
||||
}: {
|
||||
authorizationUrl: string;
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectUri: string | null;
|
||||
scope: string | null;
|
||||
state: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
pkce: {
|
||||
challengeMethod: string | null;
|
||||
codeVerifier: string | null;
|
||||
} | null;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody,
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
if (pkce) {
|
||||
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
|
||||
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
|
||||
authorizationUrl.searchParams.set('code_challenge', createPkceCodeChallenge(verifier, challengeMethod));
|
||||
authorizationUrl.searchParams.set('code_challenge_method', challengeMethod);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log('Authorizing', authorizationUrlStr);
|
||||
|
||||
let foundCode = false;
|
||||
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: 'oauth-authorization-url',
|
||||
dataDirKey: await getDataDirKey(ctx, contextId),
|
||||
async onClose() {
|
||||
if (!foundCode) {
|
||||
reject(new Error('Authorization window closed'));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has('error')) {
|
||||
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
|
||||
}
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
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();
|
||||
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'code', value: code },
|
||||
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createPkceCodeVerifier() {
|
||||
return encodeForPkce(randomBytes(32));
|
||||
}
|
||||
|
||||
function createPkceCodeChallenge(verifier: string, method: string) {
|
||||
if (method === 'plain') {
|
||||
return verifier;
|
||||
}
|
||||
|
||||
const hash = encodeForPkce(createHash('sha256').update(verifier).digest());
|
||||
return hash
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
}
|
||||
|
||||
function encodeForPkce(bytes: Buffer) {
|
||||
return bytes.toString('base64')
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
}
|
||||
43
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
43
plugins/auth-oauth2/src/grants/clientCredentials.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getToken, storeToken } from '../store';
|
||||
|
||||
export async function getClientCredentials(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
}: {
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
// resolve(token.response.access_token);
|
||||
// TODO: Refresh token if expired
|
||||
// return;
|
||||
}
|
||||
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsInBody,
|
||||
params: [],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
81
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
81
plugins/auth-oauth2/src/grants/implicit.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store';
|
||||
|
||||
export function getImplicit(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
responseType,
|
||||
clientId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
}: {
|
||||
authorizationUrl: string;
|
||||
responseType: string;
|
||||
clientId: string;
|
||||
redirectUri: string | null;
|
||||
scope: string | null;
|
||||
state: string | null;
|
||||
audience: string | null;
|
||||
},
|
||||
) :Promise<AccessToken> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
// resolve(token.response.access_token);
|
||||
// TODO: Refresh token if expired
|
||||
// return;
|
||||
}
|
||||
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl.searchParams.set('response_type', 'token');
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
if (responseType.includes('id_token')) {
|
||||
authorizationUrl.searchParams.set('nonce', String(Math.floor(Math.random() * 9999999999999) + 1));
|
||||
}
|
||||
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
let foundAccessToken = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: 'oauth-authorization-url',
|
||||
async onClose() {
|
||||
if (!foundAccessToken) {
|
||||
reject(new Error('Authorization window closed'));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has('error')) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get('error')}`));
|
||||
}
|
||||
|
||||
const hash = url.hash.slice(1);
|
||||
const params = new URLSearchParams(hash);
|
||||
|
||||
const accessToken = params.get('access_token');
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
foundAccessToken = true;
|
||||
|
||||
// Close the window here, because we don't need it anymore
|
||||
close();
|
||||
|
||||
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
55
plugins/auth-oauth2/src/grants/password.ts
Normal file
55
plugins/auth-oauth2/src/grants/password.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
import { getAccessToken } from '../getAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import { AccessToken, storeToken } from '../store';
|
||||
|
||||
export async function getPassword(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
{
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
credentialsInBody,
|
||||
audience,
|
||||
scope,
|
||||
}: {
|
||||
accessTokenUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scope: string | null;
|
||||
audience: string | null;
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody,
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const response = await getAccessToken(ctx, {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
grantType: 'password',
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'username', value: username },
|
||||
{ name: 'password', value: password },
|
||||
],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
332
plugins/auth-oauth2/src/index.ts
Normal file
332
plugins/auth-oauth2/src/index.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import {
|
||||
Context,
|
||||
FormInputSelectOption,
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
import { DEFAULT_PKCE_METHOD, getAuthorizationCode, PKCE_PLAIN, PKCE_SHA256 } from './grants/authorizationCode';
|
||||
import { getClientCredentials } from './grants/clientCredentials';
|
||||
import { getImplicit } from './grants/implicit';
|
||||
import { getPassword } from './grants/password';
|
||||
import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store';
|
||||
|
||||
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
|
||||
|
||||
const grantTypes: FormInputSelectOption[] = [
|
||||
{ label: 'Authorization Code', value: 'authorization_code' },
|
||||
{ label: 'Implicit', value: 'implicit' },
|
||||
{ label: 'Resource Owner Password Credential', value: 'password' },
|
||||
{ label: 'Client Credentials', value: 'client_credentials' },
|
||||
];
|
||||
|
||||
const defaultGrantType = grantTypes[0]!.value;
|
||||
|
||||
function hiddenIfNot(grantTypes: GrantType[], ...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]) {
|
||||
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
|
||||
const hasGrantType = grantTypes.find(t => t === String(values.grantType ?? defaultGrantType));
|
||||
const hasOtherBools = other.every(t => t(values));
|
||||
const show = hasGrantType && hasOtherBools;
|
||||
return { hidden: !show };
|
||||
};
|
||||
}
|
||||
|
||||
const authorizationUrls = [
|
||||
'https://github.com/login/oauth/authorize',
|
||||
'https://account.box.com/api/oauth2/authorize',
|
||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
'https://api.imgur.com/oauth2/authorize',
|
||||
'https://bitly.com/oauth/authorize',
|
||||
'https://gitlab.example.com/oauth/authorize',
|
||||
'https://medium.com/m/oauth/authorize',
|
||||
'https://public-api.wordpress.com/oauth2/authorize',
|
||||
'https://slack.com/oauth/authorize',
|
||||
'https://todoist.com/oauth/authorize',
|
||||
'https://www.dropbox.com/oauth2/authorize',
|
||||
'https://www.linkedin.com/oauth/v2/authorization',
|
||||
'https://MY_SHOP.myshopify.com/admin/oauth/access_token',
|
||||
];
|
||||
|
||||
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',
|
||||
];
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
],
|
||||
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: 'checkbox',
|
||||
name: 'usePkce',
|
||||
label: 'Use PKCE',
|
||||
dynamic: hiddenIfNot(['authorization_code']),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'pkceChallengeMethod',
|
||||
label: 'Code Challenge Method',
|
||||
options: [{ label: 'SHA-256', value: PKCE_SHA256 }, { label: 'Plain', value: PKCE_PLAIN }],
|
||||
defaultValue: DEFAULT_PKCE_METHOD,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pkceCodeVerifier',
|
||||
label: 'Code Verifier',
|
||||
placeholder: 'Automatically generated if not provided',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
password: true,
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'responseType',
|
||||
label: 'Response Type',
|
||||
defaultValue: 'token',
|
||||
options: [
|
||||
{ label: 'Access Token', value: 'token' },
|
||||
{ label: 'ID Token', value: 'id_token' },
|
||||
{ label: 'ID and Access Token', value: 'id_token token' },
|
||||
],
|
||||
dynamic: hiddenIfNot(['implicit']),
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
inputs: [
|
||||
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
|
||||
{ type: 'text', name: 'headerPrefix', label: 'Header Prefix', optional: true, defaultValue: 'Bearer' },
|
||||
{
|
||||
type: 'select', name: 'credentials', label: 'Send Credentials', defaultValue: 'body', options: [
|
||||
{ label: 'In Request Body', value: 'body' },
|
||||
{ label: 'As Basic Authentication', value: 'basic' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Access Token Response',
|
||||
async dynamic(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return { hidden: true };
|
||||
}
|
||||
return {
|
||||
label: 'Access Token Response',
|
||||
inputs: [
|
||||
{
|
||||
type: 'editor',
|
||||
defaultValue: JSON.stringify(token.response, null, 2),
|
||||
hideLabel: true,
|
||||
readOnly: true,
|
||||
language: 'json',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
async onApply(ctx, { values, contextId }) {
|
||||
const headerPrefix = stringArg(values, 'headerPrefix');
|
||||
const grantType = stringArg(values, 'grantType') as GrantType;
|
||||
const credentialsInBody = values.credentials === 'body';
|
||||
|
||||
let token: AccessToken;
|
||||
if (grantType === 'authorization_code') {
|
||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
token = await getAuthorizationCode(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
state: stringArgOrNull(values, 'state'),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce ? {
|
||||
challengeMethod: stringArg(values, 'pkceChallengeMethod'),
|
||||
codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'),
|
||||
} : null,
|
||||
});
|
||||
} 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'),
|
||||
});
|
||||
} 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.access_token}`.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;
|
||||
}
|
||||
56
plugins/auth-oauth2/src/store.ts
Normal file
56
plugins/auth-oauth2/src/store.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Context } from '@yaakapp/api';
|
||||
|
||||
export async function storeToken(ctx: Context, contextId: string, response: AccessTokenRawResponse) {
|
||||
if (!response.access_token) {
|
||||
throw new Error(`Token not found in response`);
|
||||
}
|
||||
|
||||
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(context_id: string) {
|
||||
return ['token', context_id].join('::');
|
||||
}
|
||||
|
||||
function dataDirStoreKey(context_id: string) {
|
||||
return ['data_dir', context_id].join('::');
|
||||
}
|
||||
|
||||
export interface AccessToken {
|
||||
response: AccessTokenRawResponse,
|
||||
expiresAt: number | null;
|
||||
}
|
||||
|
||||
export interface AccessTokenRawResponse {
|
||||
access_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 `));
|
||||
});
|
||||
});
|
||||
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!' }],
|
||||
}],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
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) });
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
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);
|
||||
}
|
||||
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,
|
||||
});
|
||||
},
|
||||
}],
|
||||
};
|
||||
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,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
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;
|
||||
}
|
||||
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();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
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;
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
25
plugins_/_package.json
Normal file
25
plugins_/_package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@yaakapp/plugins",
|
||||
"repository": "https://github.com/yaakapp/plugins",
|
||||
"workspaces": [
|
||||
"plugins/*"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "workspaces-run --parallel -- npm run --workspaces build",
|
||||
"dev": "workspaces-run --parallel -- npm run --workspaces dev",
|
||||
"test": "vitest run",
|
||||
"lint": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.4",
|
||||
"jsonpath": "^1.1.1",
|
||||
"typescript": "^5.6.2",
|
||||
"vitest": "^2.0.4",
|
||||
"workspaces-run": "^1.0.2",
|
||||
"@yaakapp/cli": "^0.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yaakapp/api": "^0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
const { readdirSync, cpSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
|
||||
if (!pluginsDir) {
|
||||
console.log('Skipping bundled plugins build because YAAK_PLUGINS_DIR is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Installing Yaak plugins dependencies', pluginsDir);
|
||||
execSync('npm ci', { cwd: pluginsDir });
|
||||
console.log('Building Yaak plugins', pluginsDir);
|
||||
execSync('npm run build', { cwd: pluginsDir });
|
||||
const pluginsDir = path.join(__dirname, '..', 'plugins');
|
||||
|
||||
console.log('Copying Yaak plugins to', pluginsDir);
|
||||
|
||||
const pluginsRoot = path.join(pluginsDir, 'plugins');
|
||||
for (const name of readdirSync(pluginsRoot)) {
|
||||
const dir = path.join(pluginsRoot, name);
|
||||
for (const name of readdirSync(pluginsDir)) {
|
||||
const dir = path.join(pluginsDir, name);
|
||||
if (name.startsWith('.')) continue;
|
||||
console.log('Building plugin', dir);
|
||||
execSync('npm run build', { cwd: dir });
|
||||
const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name);
|
||||
console.log(`Copying ${name} to ${destDir}`);
|
||||
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
||||
|
||||
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
target/
|
||||
|
||||
vendored/*
|
||||
!vendored/plugins
|
||||
|
||||
gen/*
|
||||
|
||||
|
||||
53
src-tauri/vendored/plugins/auth-basic/build/index.js
generated
53
src-tauri/vendored/plugins/auth-basic/build/index.js
generated
@@ -1,53 +0,0 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var plugin = {
|
||||
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 }] };
|
||||
}
|
||||
}
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var plugin = {
|
||||
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 }] };
|
||||
}
|
||||
}
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
3881
src-tauri/vendored/plugins/auth-jwt/build/index.js
generated
3881
src-tauri/vendored/plugins/auth-jwt/build/index.js
generated
File diff suppressed because it is too large
Load Diff
728
src-tauri/vendored/plugins/auth-oauth2/build/index.js
generated
728
src-tauri/vendored/plugins/auth-oauth2/build/index.js
generated
@@ -1,728 +0,0 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
|
||||
// src/grants/authorizationCode.ts
|
||||
var import_node_crypto = require("node:crypto");
|
||||
|
||||
// src/getAccessToken.ts
|
||||
var import_node_fs = require("node:fs");
|
||||
async function getAccessToken(ctx, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
audience,
|
||||
params,
|
||||
grantType,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret
|
||||
}) {
|
||||
console.log("Getting access token", accessTokenUrl);
|
||||
const 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 (scope) 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 });
|
||||
}
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
const body = resp.bodyPath ? (0, import_node_fs.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;
|
||||
}
|
||||
|
||||
// src/getOrRefreshAccessToken.ts
|
||||
var import_node_fs2 = require("node:fs");
|
||||
|
||||
// src/store.ts
|
||||
async function storeToken(ctx, contextId, response) {
|
||||
if (!response.access_token) {
|
||||
throw new Error(`Token not found in response`);
|
||||
}
|
||||
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : null;
|
||||
const token = {
|
||||
response,
|
||||
expiresAt
|
||||
};
|
||||
await ctx.store.set(tokenStoreKey(contextId), token);
|
||||
return token;
|
||||
}
|
||||
async function getToken(ctx, contextId) {
|
||||
return ctx.store.get(tokenStoreKey(contextId));
|
||||
}
|
||||
async function deleteToken(ctx, contextId) {
|
||||
return ctx.store.delete(tokenStoreKey(contextId));
|
||||
}
|
||||
async function resetDataDirKey(ctx, contextId) {
|
||||
const key = (/* @__PURE__ */ new Date()).toISOString();
|
||||
return ctx.store.set(dataDirStoreKey(contextId), key);
|
||||
}
|
||||
async function getDataDirKey(ctx, contextId) {
|
||||
const key = await ctx.store.get(dataDirStoreKey(contextId)) ?? "default";
|
||||
return `${contextId}::${key}`;
|
||||
}
|
||||
function tokenStoreKey(context_id) {
|
||||
return ["token", context_id].join("::");
|
||||
}
|
||||
function dataDirStoreKey(context_id) {
|
||||
return ["data_dir", context_id].join("::");
|
||||
}
|
||||
|
||||
// src/getOrRefreshAccessToken.ts
|
||||
async function getOrRefreshAccessToken(ctx, contextId, {
|
||||
scope,
|
||||
accessTokenUrl,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
forceRefresh
|
||||
}) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
const now = Date.now();
|
||||
const isExpired = token.expiresAt && now > token.expiresAt;
|
||||
if (!isExpired && !forceRefresh) {
|
||||
return token;
|
||||
}
|
||||
if (!token.response.refresh_token) {
|
||||
return null;
|
||||
}
|
||||
const 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 });
|
||||
}
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
if (resp.status === 401) {
|
||||
console.log("Unauthorized refresh_token request");
|
||||
await deleteToken(ctx, contextId);
|
||||
return null;
|
||||
}
|
||||
const body = resp.bodyPath ? (0, import_node_fs2.readFileSync)(resp.bodyPath, "utf8") : "";
|
||||
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 = {
|
||||
...response,
|
||||
// Assign a new one or keep the old one,
|
||||
refresh_token: response.refresh_token ?? token.response.refresh_token
|
||||
};
|
||||
return storeToken(ctx, contextId, newResponse);
|
||||
}
|
||||
|
||||
// src/grants/authorizationCode.ts
|
||||
var PKCE_SHA256 = "S256";
|
||||
var PKCE_PLAIN = "plain";
|
||||
var DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
||||
async function getAuthorizationCode(ctx, contextId, {
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
pkce
|
||||
}) {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
|
||||
authorizationUrl.searchParams.set("response_type", "code");
|
||||
authorizationUrl.searchParams.set("client_id", clientId);
|
||||
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (audience) authorizationUrl.searchParams.set("audience", audience);
|
||||
if (pkce) {
|
||||
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
|
||||
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
|
||||
authorizationUrl.searchParams.set("code_challenge", createPkceCodeChallenge(verifier, challengeMethod));
|
||||
authorizationUrl.searchParams.set("code_challenge_method", challengeMethod);
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log("Authorizing", authorizationUrlStr);
|
||||
let foundCode = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: "oauth-authorization-url",
|
||||
dataDirKey: await getDataDirKey(ctx, contextId),
|
||||
async onClose() {
|
||||
if (!foundCode) {
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has("error")) {
|
||||
return reject(new Error(`Failed to authorize: ${url.searchParams.get("error")}`));
|
||||
}
|
||||
const code = url.searchParams.get("code");
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
foundCode = true;
|
||||
close();
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: "authorization_code",
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: "code", value: code },
|
||||
...redirectUri ? [{ name: "redirect_uri", value: redirectUri }] : []
|
||||
]
|
||||
});
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function createPkceCodeVerifier() {
|
||||
return encodeForPkce((0, import_node_crypto.randomBytes)(32));
|
||||
}
|
||||
function createPkceCodeChallenge(verifier, method) {
|
||||
if (method === "plain") {
|
||||
return verifier;
|
||||
}
|
||||
const hash = encodeForPkce((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
|
||||
return hash.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
function encodeForPkce(bytes) {
|
||||
return bytes.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
// src/grants/clientCredentials.ts
|
||||
async function getClientCredentials(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
credentialsInBody
|
||||
}) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token) {
|
||||
}
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: "client_credentials",
|
||||
accessTokenUrl,
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsInBody,
|
||||
params: []
|
||||
});
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
|
||||
// src/grants/implicit.ts
|
||||
function getImplicit(ctx, contextId, {
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
responseType,
|
||||
clientId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience
|
||||
}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (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));
|
||||
}
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
let foundAccessToken = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: "oauth-authorization-url",
|
||||
async onClose() {
|
||||
if (!foundAccessToken) {
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has("error")) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
|
||||
}
|
||||
const hash = url.hash.slice(1);
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get("access_token");
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
foundAccessToken = true;
|
||||
close();
|
||||
const response = Object.fromEntries(params);
|
||||
try {
|
||||
resolve(await storeToken(ctx, contextId, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// src/grants/password.ts
|
||||
async function getPassword(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
credentialsInBody,
|
||||
audience,
|
||||
scope
|
||||
}) {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
credentialsInBody
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
const response = await getAccessToken(ctx, {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
grantType: "password",
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: "username", value: username },
|
||||
{ name: "password", value: password }
|
||||
]
|
||||
});
|
||||
return storeToken(ctx, contextId, response);
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var grantTypes = [
|
||||
{ label: "Authorization Code", value: "authorization_code" },
|
||||
{ label: "Implicit", value: "implicit" },
|
||||
{ label: "Resource Owner Password Credential", value: "password" },
|
||||
{ label: "Client Credentials", value: "client_credentials" }
|
||||
];
|
||||
var defaultGrantType = grantTypes[0].value;
|
||||
function hiddenIfNot(grantTypes2, ...other) {
|
||||
return (_ctx, { values }) => {
|
||||
const hasGrantType = grantTypes2.find((t) => t === String(values.grantType ?? defaultGrantType));
|
||||
const hasOtherBools = other.every((t) => t(values));
|
||||
const show = hasGrantType && hasOtherBools;
|
||||
return { hidden: !show };
|
||||
};
|
||||
}
|
||||
var 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"
|
||||
];
|
||||
var 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"
|
||||
];
|
||||
var plugin = {
|
||||
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);
|
||||
}
|
||||
}
|
||||
],
|
||||
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: "checkbox",
|
||||
name: "usePkce",
|
||||
label: "Use PKCE",
|
||||
dynamic: hiddenIfNot(["authorization_code"])
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "pkceChallengeMethod",
|
||||
label: "Code Challenge Method",
|
||||
options: [{ label: "SHA-256", value: PKCE_SHA256 }, { label: "Plain", value: PKCE_PLAIN }],
|
||||
defaultValue: DEFAULT_PKCE_METHOD,
|
||||
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "pkceCodeVerifier",
|
||||
label: "Code Verifier",
|
||||
placeholder: "Automatically generated if not provided",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "username",
|
||||
label: "Username",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(["password"])
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "password",
|
||||
label: "Password",
|
||||
password: true,
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(["password"])
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "responseType",
|
||||
label: "Response Type",
|
||||
defaultValue: "token",
|
||||
options: [
|
||||
{ label: "Access Token", value: "token" },
|
||||
{ label: "ID Token", value: "id_token" },
|
||||
{ label: "ID and Access Token", value: "id_token token" }
|
||||
],
|
||||
dynamic: hiddenIfNot(["implicit"])
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{ type: "text", name: "scope", label: "Scope", optional: true },
|
||||
{ type: "text", name: "headerPrefix", label: "Header Prefix", optional: true, defaultValue: "Bearer" },
|
||||
{
|
||||
type: "select",
|
||||
name: "credentials",
|
||||
label: "Send Credentials",
|
||||
defaultValue: "body",
|
||||
options: [
|
||||
{ label: "In Request Body", value: "body" },
|
||||
{ label: "As Basic Authentication", value: "basic" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
label: "Access Token Response",
|
||||
async dynamic(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
return { hidden: true };
|
||||
}
|
||||
return {
|
||||
label: "Access Token Response",
|
||||
inputs: [
|
||||
{
|
||||
type: "editor",
|
||||
defaultValue: JSON.stringify(token.response, null, 2),
|
||||
hideLabel: true,
|
||||
readOnly: true,
|
||||
language: "json"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
async onApply(ctx, { values, contextId }) {
|
||||
const headerPrefix = stringArg(values, "headerPrefix");
|
||||
const grantType = stringArg(values, "grantType");
|
||||
const credentialsInBody = values.credentials === "body";
|
||||
let token;
|
||||
if (grantType === "authorization_code") {
|
||||
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getAuthorizationCode(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
state: stringArgOrNull(values, "state"),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce ? {
|
||||
challengeMethod: stringArg(values, "pkceChallengeMethod"),
|
||||
codeVerifier: stringArgOrNull(values, "pkceCodeVerifier")
|
||||
} : null
|
||||
});
|
||||
} 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")
|
||||
});
|
||||
} 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.access_token}`.trim();
|
||||
return {
|
||||
setHeaders: [{
|
||||
name: "Authorization",
|
||||
value: headerValue
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
function stringArgOrNull(values, name) {
|
||||
const arg = values[name];
|
||||
if (arg == null || arg == "") return null;
|
||||
return `${arg}`;
|
||||
}
|
||||
function stringArg(values, name) {
|
||||
const arg = stringArgOrNull(values, name);
|
||||
if (!arg) return "";
|
||||
return arg;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
plugin
|
||||
});
|
||||
108
src-tauri/vendored/plugins/exporter-curl/build/index.js
generated
108
src-tauri/vendored/plugins/exporter-curl/build/index.js
generated
@@ -1,108 +0,0 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
convertToCurl: () => convertToCurl,
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var NEWLINE = "\\\n ";
|
||||
var plugin = {
|
||||
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" });
|
||||
}
|
||||
}]
|
||||
};
|
||||
async function convertToCurl(request) {
|
||||
const xs = ["curl"];
|
||||
if (request.method) xs.push("-X", request.method);
|
||||
if (request.url) xs.push(quote(request.url));
|
||||
xs.push(NEWLINE);
|
||||
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
|
||||
xs.push("--url-query", quote(`${p.name}=${p.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
|
||||
xs.push("--header", quote(`${h.name}: ${h.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
if (Array.isArray(request.body?.form)) {
|
||||
const flag = request.bodyType === "multipart/form-data" ? "--form" : "--data";
|
||||
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
|
||||
if (p.file) {
|
||||
let v = `${p.name}=@${p.file}`;
|
||||
v += p.contentType ? `;type=${p.contentType}` : "";
|
||||
xs.push(flag, v);
|
||||
} else {
|
||||
xs.push(flag, quote(`${p.name}=${p.value}`));
|
||||
}
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
} else if (typeof request.body?.query === "string") {
|
||||
const body = { query: request.body.query || "", variables: maybeParseJSON(request.body.variables, void 0) };
|
||||
xs.push("--data-raw", `${quote(JSON.stringify(body))}`);
|
||||
xs.push(NEWLINE);
|
||||
} else if (typeof request.body?.text === "string") {
|
||||
xs.push("--data-raw", `${quote(request.body.text)}`);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
if (request.authenticationType === "basic" || request.authenticationType === "digest") {
|
||||
if (request.authenticationType === "digest") xs.push("--digest");
|
||||
xs.push(
|
||||
"--user",
|
||||
quote(`${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`)
|
||||
);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
if (request.authenticationType === "bearer") {
|
||||
xs.push("--header", quote(`Authorization: Bearer ${request.authentication?.token ?? ""}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
if (xs[xs.length - 1] === NEWLINE) {
|
||||
xs.splice(xs.length - 1, 1);
|
||||
}
|
||||
return xs.join(" ");
|
||||
}
|
||||
function quote(arg) {
|
||||
const escaped = arg.replace(/'/g, "\\'");
|
||||
return `'${escaped}'`;
|
||||
}
|
||||
function onlyEnabled(v) {
|
||||
return v.enabled !== false && !!v.name;
|
||||
}
|
||||
function maybeParseJSON(v, fallback) {
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
convertToCurl,
|
||||
plugin
|
||||
});
|
||||
1739
src-tauri/vendored/plugins/filter-jsonpath/build/index.js
generated
1739
src-tauri/vendored/plugins/filter-jsonpath/build/index.js
generated
File diff suppressed because it is too large
Load Diff
8372
src-tauri/vendored/plugins/filter-xpath/build/index.js
generated
8372
src-tauri/vendored/plugins/filter-xpath/build/index.js
generated
File diff suppressed because it is too large
Load Diff
591
src-tauri/vendored/plugins/importer-curl/build/index.js
generated
591
src-tauri/vendored/plugins/importer-curl/build/index.js
generated
@@ -1,591 +0,0 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// ../../node_modules/shell-quote/quote.js
|
||||
var require_quote = __commonJS({
|
||||
"../../node_modules/shell-quote/quote.js"(exports2, module2) {
|
||||
"use strict";
|
||||
module2.exports = function quote(xs) {
|
||||
return xs.map(function(s) {
|
||||
if (s === "") {
|
||||
return "''";
|
||||
}
|
||||
if (s && typeof s === "object") {
|
||||
return s.op.replace(/(.)/g, "\\$1");
|
||||
}
|
||||
if (/["\s]/.test(s) && !/'/.test(s)) {
|
||||
return "'" + s.replace(/(['\\])/g, "\\$1") + "'";
|
||||
}
|
||||
if (/["'\s]/.test(s)) {
|
||||
return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"';
|
||||
}
|
||||
return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
|
||||
}).join(" ");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ../../node_modules/shell-quote/parse.js
|
||||
var require_parse = __commonJS({
|
||||
"../../node_modules/shell-quote/parse.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var CONTROL = "(?:" + [
|
||||
"\\|\\|",
|
||||
"\\&\\&",
|
||||
";;",
|
||||
"\\|\\&",
|
||||
"\\<\\(",
|
||||
"\\<\\<\\<",
|
||||
">>",
|
||||
">\\&",
|
||||
"<\\&",
|
||||
"[&;()|<>]"
|
||||
].join("|") + ")";
|
||||
var controlRE = new RegExp("^" + CONTROL + "$");
|
||||
var META = "|&;()<> \\t";
|
||||
var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
|
||||
var DOUBLE_QUOTE = "'((\\\\'|[^'])*?)'";
|
||||
var hash = /^#$/;
|
||||
var SQ = "'";
|
||||
var DQ = '"';
|
||||
var DS = "$";
|
||||
var TOKEN = "";
|
||||
var mult = 4294967296;
|
||||
for (i = 0; i < 4; i++) {
|
||||
TOKEN += (mult * Math.random()).toString(16);
|
||||
}
|
||||
var i;
|
||||
var startsWithToken = new RegExp("^" + TOKEN);
|
||||
function matchAll(s, r) {
|
||||
var origIndex = r.lastIndex;
|
||||
var matches = [];
|
||||
var matchObj;
|
||||
while (matchObj = r.exec(s)) {
|
||||
matches.push(matchObj);
|
||||
if (r.lastIndex === matchObj.index) {
|
||||
r.lastIndex += 1;
|
||||
}
|
||||
}
|
||||
r.lastIndex = origIndex;
|
||||
return matches;
|
||||
}
|
||||
function getVar(env, pre, key) {
|
||||
var r = typeof env === "function" ? env(key) : env[key];
|
||||
if (typeof r === "undefined" && key != "") {
|
||||
r = "";
|
||||
} else if (typeof r === "undefined") {
|
||||
r = "$";
|
||||
}
|
||||
if (typeof r === "object") {
|
||||
return pre + TOKEN + JSON.stringify(r) + TOKEN;
|
||||
}
|
||||
return pre + r;
|
||||
}
|
||||
function parseInternal(string, env, opts) {
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
var BS = opts.escape || "\\";
|
||||
var BAREWORD = "(\\" + BS + `['"` + META + `]|[^\\s'"` + META + "])+";
|
||||
var chunker = new RegExp([
|
||||
"(" + CONTROL + ")",
|
||||
// control chars
|
||||
"(" + BAREWORD + "|" + SINGLE_QUOTE + "|" + DOUBLE_QUOTE + ")+"
|
||||
].join("|"), "g");
|
||||
var matches = matchAll(string, chunker);
|
||||
if (matches.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!env) {
|
||||
env = {};
|
||||
}
|
||||
var commented = false;
|
||||
return matches.map(function(match) {
|
||||
var s = match[0];
|
||||
if (!s || commented) {
|
||||
return void 0;
|
||||
}
|
||||
if (controlRE.test(s)) {
|
||||
return { op: s };
|
||||
}
|
||||
var quote = false;
|
||||
var esc = false;
|
||||
var out = "";
|
||||
var isGlob = false;
|
||||
var i2;
|
||||
function parseEnvVar() {
|
||||
i2 += 1;
|
||||
var varend;
|
||||
var varname;
|
||||
var char = s.charAt(i2);
|
||||
if (char === "{") {
|
||||
i2 += 1;
|
||||
if (s.charAt(i2) === "}") {
|
||||
throw new Error("Bad substitution: " + s.slice(i2 - 2, i2 + 1));
|
||||
}
|
||||
varend = s.indexOf("}", i2);
|
||||
if (varend < 0) {
|
||||
throw new Error("Bad substitution: " + s.slice(i2));
|
||||
}
|
||||
varname = s.slice(i2, varend);
|
||||
i2 = varend;
|
||||
} else if (/[*@#?$!_-]/.test(char)) {
|
||||
varname = char;
|
||||
i2 += 1;
|
||||
} else {
|
||||
var slicedFromI = s.slice(i2);
|
||||
varend = slicedFromI.match(/[^\w\d_]/);
|
||||
if (!varend) {
|
||||
varname = slicedFromI;
|
||||
i2 = s.length;
|
||||
} else {
|
||||
varname = slicedFromI.slice(0, varend.index);
|
||||
i2 += varend.index - 1;
|
||||
}
|
||||
}
|
||||
return getVar(env, "", varname);
|
||||
}
|
||||
for (i2 = 0; i2 < s.length; i2++) {
|
||||
var c = s.charAt(i2);
|
||||
isGlob = isGlob || !quote && (c === "*" || c === "?");
|
||||
if (esc) {
|
||||
out += c;
|
||||
esc = false;
|
||||
} else if (quote) {
|
||||
if (c === quote) {
|
||||
quote = false;
|
||||
} else if (quote == SQ) {
|
||||
out += c;
|
||||
} else {
|
||||
if (c === BS) {
|
||||
i2 += 1;
|
||||
c = s.charAt(i2);
|
||||
if (c === DQ || c === BS || c === DS) {
|
||||
out += c;
|
||||
} else {
|
||||
out += BS + c;
|
||||
}
|
||||
} else if (c === DS) {
|
||||
out += parseEnvVar();
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
} else if (c === DQ || c === SQ) {
|
||||
quote = c;
|
||||
} else if (controlRE.test(c)) {
|
||||
return { op: s };
|
||||
} else if (hash.test(c)) {
|
||||
commented = true;
|
||||
var commentObj = { comment: string.slice(match.index + i2 + 1) };
|
||||
if (out.length) {
|
||||
return [out, commentObj];
|
||||
}
|
||||
return [commentObj];
|
||||
} else if (c === BS) {
|
||||
esc = true;
|
||||
} else if (c === DS) {
|
||||
out += parseEnvVar();
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
if (isGlob) {
|
||||
return { op: "glob", pattern: out };
|
||||
}
|
||||
return out;
|
||||
}).reduce(function(prev, arg) {
|
||||
return typeof arg === "undefined" ? prev : prev.concat(arg);
|
||||
}, []);
|
||||
}
|
||||
module2.exports = function parse2(s, env, opts) {
|
||||
var mapped = parseInternal(s, env, opts);
|
||||
if (typeof env !== "function") {
|
||||
return mapped;
|
||||
}
|
||||
return mapped.reduce(function(acc, s2) {
|
||||
if (typeof s2 === "object") {
|
||||
return acc.concat(s2);
|
||||
}
|
||||
var xs = s2.split(RegExp("(" + TOKEN + ".*?" + TOKEN + ")", "g"));
|
||||
if (xs.length === 1) {
|
||||
return acc.concat(xs[0]);
|
||||
}
|
||||
return acc.concat(xs.filter(Boolean).map(function(x) {
|
||||
if (startsWithToken.test(x)) {
|
||||
return JSON.parse(x.split(TOKEN)[1]);
|
||||
}
|
||||
return x;
|
||||
}));
|
||||
}, []);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ../../node_modules/shell-quote/index.js
|
||||
var require_shell_quote = __commonJS({
|
||||
"../../node_modules/shell-quote/index.js"(exports2) {
|
||||
"use strict";
|
||||
exports2.quote = require_quote();
|
||||
exports2.parse = require_parse();
|
||||
}
|
||||
});
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
convertCurl: () => convertCurl,
|
||||
plugin: () => plugin
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
var import_shell_quote = __toESM(require_shell_quote());
|
||||
var DATA_FLAGS = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"];
|
||||
var SUPPORTED_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);
|
||||
var BOOLEAN_FLAGS = ["G", "get", "digest"];
|
||||
var plugin = {
|
||||
importer: {
|
||||
name: "cURL",
|
||||
description: "Import cURL commands",
|
||||
onImport(_ctx, args) {
|
||||
return convertCurl(args.text);
|
||||
}
|
||||
}
|
||||
};
|
||||
function convertCurl(rawData) {
|
||||
if (!rawData.match(/^\s*curl /)) {
|
||||
return null;
|
||||
}
|
||||
const commands = [];
|
||||
const normalizedData = rawData.replace(/\ncurl/g, "; curl");
|
||||
let currentCommand = [];
|
||||
const parsed = (0, import_shell_quote.parse)(normalizedData);
|
||||
const normalizedParseEntries = parsed.flatMap((entry) => {
|
||||
if (typeof entry === "string" && entry.startsWith("-") && !entry.startsWith("--") && entry.length > 2) {
|
||||
return [entry.slice(0, 2), entry.slice(2)];
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
for (const parseEntry of normalizedParseEntries) {
|
||||
if (typeof parseEntry === "string") {
|
||||
if (parseEntry.startsWith("$")) {
|
||||
currentCommand.push(parseEntry.slice(1));
|
||||
} else {
|
||||
currentCommand.push(parseEntry);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ("comment" in parseEntry) {
|
||||
continue;
|
||||
}
|
||||
const { op } = parseEntry;
|
||||
if (op === ";") {
|
||||
commands.push(currentCommand);
|
||||
currentCommand = [];
|
||||
continue;
|
||||
}
|
||||
if (op?.startsWith("$")) {
|
||||
const str = op.slice(2, op.length - 1).replace(/\\'/g, "'");
|
||||
currentCommand.push(str);
|
||||
continue;
|
||||
}
|
||||
if (op === "glob") {
|
||||
currentCommand.push(parseEntry.pattern);
|
||||
}
|
||||
}
|
||||
commands.push(currentCommand);
|
||||
const workspace = {
|
||||
model: "workspace",
|
||||
id: generateId("workspace"),
|
||||
name: "Curl Import"
|
||||
};
|
||||
const requests = commands.filter((command) => command[0] === "curl").map((v) => importCommand(v, workspace.id));
|
||||
return {
|
||||
resources: {
|
||||
httpRequests: requests,
|
||||
workspaces: [workspace]
|
||||
}
|
||||
};
|
||||
}
|
||||
function importCommand(parseEntries, workspaceId) {
|
||||
const flagsByName = {};
|
||||
const singletons = [];
|
||||
for (let i = 1; i < parseEntries.length; i++) {
|
||||
let parseEntry = parseEntries[i];
|
||||
if (typeof parseEntry === "string") {
|
||||
parseEntry = parseEntry.trim();
|
||||
}
|
||||
if (typeof parseEntry === "string" && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === "-" && parseEntry[1] !== "-";
|
||||
let name = parseEntry.replace(/^-{1,2}/, "");
|
||||
if (!SUPPORTED_FLAGS.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
let value;
|
||||
const nextEntry = parseEntries[i + 1];
|
||||
const hasValue = !BOOLEAN_FLAGS.includes(name);
|
||||
if (isSingleDash && name.length > 1) {
|
||||
value = name.slice(1);
|
||||
name = name.slice(0, 1);
|
||||
} else if (typeof nextEntry === "string" && hasValue && !nextEntry.startsWith("-")) {
|
||||
value = nextEntry;
|
||||
i++;
|
||||
} else {
|
||||
value = true;
|
||||
}
|
||||
flagsByName[name] = flagsByName[name] || [];
|
||||
flagsByName[name].push(value);
|
||||
} else if (parseEntry) {
|
||||
singletons.push(parseEntry);
|
||||
}
|
||||
}
|
||||
let urlParameters;
|
||||
let url;
|
||||
const urlArg = getPairValue(flagsByName, singletons[0] || "", ["url"]);
|
||||
const [baseUrl, search] = splitOnce(urlArg, "?");
|
||||
urlParameters = search?.split("&").map((p) => {
|
||||
const v = splitOnce(p, "=");
|
||||
return { name: decodeURIComponent(v[0] ?? ""), value: decodeURIComponent(v[1] ?? ""), enabled: true };
|
||||
}) ?? [];
|
||||
url = baseUrl ?? urlArg;
|
||||
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
|
||||
});
|
||||
}
|
||||
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()
|
||||
} : {};
|
||||
const headers = [
|
||||
...flagsByName["header"] || [],
|
||||
...flagsByName["H"] || []
|
||||
].map((header) => {
|
||||
const [name, value] = header.split(/:(.*)$/);
|
||||
if (!value) {
|
||||
return {
|
||||
name: (name ?? "").trim().replace(/;$/, ""),
|
||||
value: "",
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: (name ?? "").trim(),
|
||||
value: value.trim(),
|
||||
enabled: true
|
||||
};
|
||||
});
|
||||
const cookieHeaderValue = [
|
||||
...flagsByName["cookie"] || [],
|
||||
...flagsByName["b"] || []
|
||||
].map((str) => {
|
||||
const name = str.split("=", 1)[0];
|
||||
const value = str.replace(`${name}=`, "");
|
||||
return `${name}=${value}`;
|
||||
}).join("; ");
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === "cookie");
|
||||
if (cookieHeaderValue && existingCookieHeader) {
|
||||
existingCookieHeader.value += `; ${cookieHeaderValue}`;
|
||||
} else if (cookieHeaderValue) {
|
||||
headers.push({
|
||||
name: "Cookie",
|
||||
value: cookieHeaderValue,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
const dataParameters = pairsToDataParameters(flagsByName);
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0] : null;
|
||||
const formDataParams = [
|
||||
...flagsByName["form"] || [],
|
||||
...flagsByName["F"] || []
|
||||
].map((str) => {
|
||||
const parts = str.split("=");
|
||||
const name = parts[0] ?? "";
|
||||
const value = parts[1] ?? "";
|
||||
const item = {
|
||||
name,
|
||||
enabled: true
|
||||
};
|
||||
if (value.indexOf("@") === 0) {
|
||||
item["file"] = value.slice(1);
|
||||
} else {
|
||||
item["value"] = value;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
let body = {};
|
||||
let bodyType = null;
|
||||
const bodyAsGET = getPairValue(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
|
||||
});
|
||||
}
|
||||
}
|
||||
let method = getPairValue(flagsByName, "", ["X", "request"]).toUpperCase();
|
||||
if (method === "" && body) {
|
||||
method = "text" in body || "form" in body ? "POST" : "GET";
|
||||
}
|
||||
const request = {
|
||||
id: generateId("http_request"),
|
||||
model: "http_request",
|
||||
workspaceId,
|
||||
name: "",
|
||||
urlParameters,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
authentication,
|
||||
authenticationType,
|
||||
body,
|
||||
bodyType,
|
||||
folderId: null,
|
||||
sortPriority: 0
|
||||
};
|
||||
return request;
|
||||
}
|
||||
function pairsToDataParameters(keyedPairs) {
|
||||
let dataParameters = [];
|
||||
for (const flagName of DATA_FLAGS) {
|
||||
const pairs = keyedPairs[flagName];
|
||||
if (!pairs || pairs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const p of pairs) {
|
||||
if (typeof p !== "string") continue;
|
||||
let params = p.split("&");
|
||||
for (const param of params) {
|
||||
const [name, value] = param.split("=");
|
||||
if (param.startsWith("@")) {
|
||||
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;
|
||||
}
|
||||
var getPairValue = (pairsByName, defaultValue, names) => {
|
||||
for (const name of names) {
|
||||
if (pairsByName[name] && pairsByName[name].length) {
|
||||
return pairsByName[name][0];
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
function splitOnce(str, sep) {
|
||||
const index = str.indexOf(sep);
|
||||
if (index > -1) {
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
}
|
||||
return [str];
|
||||
}
|
||||
var idCount = {};
|
||||
function generateId(model) {
|
||||
idCount[model] = (idCount[model] ?? -1) + 1;
|
||||
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
convertCurl,
|
||||
plugin
|
||||
});
|
||||
7759
src-tauri/vendored/plugins/importer-insomnia/build/index.js
generated
7759
src-tauri/vendored/plugins/importer-insomnia/build/index.js
generated
File diff suppressed because it is too large
Load Diff
146558
src-tauri/vendored/plugins/importer-openapi/build/index.js
generated
146558
src-tauri/vendored/plugins/importer-openapi/build/index.js
generated
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user