Move plugins to this repo

This commit is contained in:
Gregory Schier
2024-06-27 21:44:45 -07:00
parent d1d08963fb
commit a63b485b95
35 changed files with 6931 additions and 0 deletions

133
.gitignore vendored Normal file
View File

@@ -0,0 +1,133 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Other
build
.idea

1544
plugins/exporter-curl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"name": "exporter-curl",
"version": "0.0.1",
"devDependencies": {
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,76 @@
import { HttpRequest } from '../../../src-web/lib/models';
const NEWLINE = '\\\n ';
export function pluginHookExport(_: any, 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?.text === 'string') {
// --data-raw $'...' to do special ANSI C quoting
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;
}

View File

@@ -0,0 +1,177 @@
import { describe, expect, test } from 'vitest';
import { pluginHookExport } from '../src';
const ctx = {};
describe('exporter-curl', () => {
test('Exports GET with params', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
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 PUT with multipart form', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
});
test('Digest auth', () => {
expect(
pluginHookExport(ctx, {
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', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'tok',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
});
test('Broken bearer auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
});
});

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

182
plugins/filter-jsonpath/package-lock.json generated Normal file
View File

@@ -0,0 +1,182 @@
{
"name": "filter-jsonpath",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "filter-jsonpath",
"version": "0.0.1",
"dependencies": {
"jsonpath": "^1.1.1"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
},
"node_modules/@types/jsonpath": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz",
"integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==",
"dev": true
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
},
"node_modules/escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=4.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/escodegen/node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esprima": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz",
"integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"node_modules/jsonpath": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz",
"integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==",
"dependencies": {
"esprima": "1.2.2",
"static-eval": "2.0.2",
"underscore": "1.12.1"
}
},
"node_modules/levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dependencies": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dependencies": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.6",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/static-eval": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
"integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==",
"dependencies": {
"escodegen": "^1.8.1"
}
},
"node_modules/type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
"dependencies": {
"prelude-ls": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/underscore": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"engines": {
"node": ">=0.10.0"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "filter-jsonpath",
"version": "0.0.1",
"dependencies": {
"jsonpath": "^1.1.1"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

@@ -0,0 +1,7 @@
import jp from 'jsonpath';
export function pluginHookResponseFilter(_ctx: any, args: { filter: string; body: string }) {
const parsed = JSON.parse(args.body);
const filtered = jp.query(parsed, args.filter);
return JSON.stringify(filtered, null, 2);
}

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

32
plugins/filter-xpath/package-lock.json generated Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "filter-xpath",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "filter-xpath",
"version": "0.0.1",
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"xpath": "^0.0.34"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/xpath": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz",
"integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==",
"engines": {
"node": ">=0.6.0"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "filter-xpath",
"version": "0.0.1",
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"xpath": "^0.0.34"
}
}

View File

@@ -0,0 +1,10 @@
import { DOMParser } from '@xmldom/xmldom';
import xpath from 'xpath';
export function pluginHookResponseFilter(
_ctx: any,
{ filter, body }: { filter: string; body: string },
) {
const doc = new DOMParser().parseFromString(body, 'text/xml');
return `${xpath.select(filter, doc)}`;
}

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

1562
plugins/importer-curl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"name": "importer-curl",
"version": "0.0.1",
"dependencies": {
"shell-quote": "^1.8.1"
},
"devDependencies": {
"@types/shell-quote": "^1.7.5",
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,419 @@
import { ControlOperator, parse, ParseEntry } from 'shell-quote';
import {
Environment,
Folder,
HttpRequest,
HttpUrlParameter,
Model,
Workspace,
} from '../../../src-web/lib/models';
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_ARGS = [
['url'], // Specify the URL explicitly
['user', 'u'], // Authentication
['digest'], // Apply auth as digest
['header', 'H'],
['cookie', 'b'],
['get', 'G'], // Put the post data in the URL
['d', 'data'], // Add url encoded data
['data-raw'],
['data-urlencode'],
['data-binary'],
['data-ascii'],
['form', 'F'], // Add multipart data
['request', 'X'], // Request method
DATA_FLAGS,
].flatMap((v) => v);
type Pair = string | boolean;
type PairsByName = Record<string, Pair[]>;
export function pluginHookImport(_: any, 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 pairsByName: PairsByName = {};
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_ARGS.includes(name)) {
continue;
}
let value;
const nextEntry = parseEntries[i + 1];
if (isSingleDash && name.length > 1) {
// Handle squished arguments like -XPOST
value = name.slice(1);
name = name.slice(0, 1);
} else if (typeof nextEntry === 'string' && !nextEntry.startsWith('-')) {
// Next arg is not a flag, so assign it as the value
value = nextEntry;
i++; // Skip next one
} else {
value = true;
}
pairsByName[name] = pairsByName[name] || [];
pairsByName[name]!.push(value);
} else if (parseEntry) {
singletons.push(parseEntry);
}
}
// ~~~~~~~~~~~~~~~~~ //
// Build the request //
// ~~~~~~~~~~~~~~~~~ //
// Url & parameters
let urlParameters: HttpUrlParameter[];
let url: string;
const urlArg = getPairValue(pairsByName, (singletons[0] as string) || '', ['url']);
const [baseUrl, search] = splitOnce(urlArg, '?');
urlParameters =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: v[0] ?? '', value: v[1] ?? '', enabled: true };
}) ?? [];
url = baseUrl ?? urlArg;
// Authentication
const [username, password] = getPairValue(pairsByName, '', ['u', 'user']).split(/:(.*)$/);
const isDigest = getPairValue(pairsByName, false, ['digest']);
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
const authentication = username
? {
username: username.trim(),
password: (password ?? '').trim(),
}
: {};
// Headers
const headers = [
...((pairsByName['header'] as string[] | undefined) || []),
...((pairsByName['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 = [
...((pairsByName['cookie'] as string[] | undefined) || []),
...((pairsByName['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(pairsByName);
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0] : null;
// Body (Multipart Form Data)
const formDataParams = [
...((pairsByName['form'] as string[] | undefined) || []),
...((pairsByName['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(pairsByName, 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(pairsByName, '', ['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: PairsByName): 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;
const [name, value] = p.split('=');
if (p.startsWith('@')) {
// Yaak doesn't support files in url-encoded data, so
dataParameters.push({
name: name ?? '',
value: '',
filePath: p.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: PairsByName,
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<Model['model'], number>> = {};
function generateId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View File

@@ -0,0 +1,341 @@
import { describe, expect, test } from 'vitest';
import { HttpRequest, Model, Workspace } from '../../../src-web/lib/models';
import { pluginHookImport } from '../src';
const ctx = {};
describe('importer-curl', () => {
test('Imports basic GET', () => {
expect(pluginHookImport(ctx, 'curl https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Explicit URL', () => {
expect(pluginHookImport(ctx, 'curl --url https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Missing URL', () => {
expect(pluginHookImport(ctx, 'curl -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
}),
],
},
});
});
test('URL between', () => {
expect(pluginHookImport(ctx, 'curl -v https://yaak.app -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Random flags', () => {
expect(pluginHookImport(ctx, 'curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Imports --request method', () => {
expect(pluginHookImport(ctx, 'curl --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports -XPOST method', () => {
expect(pluginHookImport(ctx, 'curl -XPOST --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports multiple requests', () => {
expect(
pluginHookImport(
ctx,
'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(
pluginHookImport(ctx, '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(pluginHookImport(ctx, '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 data params as text', () => {
expect(
pluginHookImport(ctx, '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 multi-line JSON', () => {
expect(
pluginHookImport(
ctx,
`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(
pluginHookImport(ctx, '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(pluginHookImport(ctx, '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(pluginHookImport(ctx, '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(pluginHookImport(ctx, '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 from the URL', () => {
expect(pluginHookImport(ctx, '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%20a', enabled: true },
],
}),
],
},
});
});
});
const idCount: Partial<Record<Model['model'], 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,
};
}

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

View File

@@ -0,0 +1,26 @@
{
"name": "importer-insomnia",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "importer-insomnia",
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "importer-insomnia",
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
}

View File

@@ -0,0 +1,280 @@
import {
Environment,
Folder,
GrpcRequest,
HttpRequest,
Workspace,
} from '../../../src-web/lib/models';
import YAML from 'yaml';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export function pluginHookImport(ctx: any, contents: string) {
let parsed: any;
try {
parsed = JSON.parse(contents);
} catch (e) {}
try {
parsed = parsed ?? YAML.parse(contents);
} catch (e) {
console.log('FAILED', e);
}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;
const resources: ExportResources = {
workspaces: [],
httpRequests: [],
grpcRequests: [],
environments: [],
folders: [],
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) {
const baseEnvironment = parsed.resources.find(
(r: any) => isEnvironment(r) && r.parentId === workspaceToImport._id,
);
resources.workspaces.push({
id: convertId(workspaceToImport._id),
createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace('Z', ''),
model: 'workspace',
name: workspaceToImport.name,
variables: baseEnvironment ? parseVariables(baseEnvironment.data) : [],
});
const environmentsToImport = parsed.resources.filter(
(r: any) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
);
resources.environments.push(
...environmentsToImport.map((r: any) => importEnvironment(r, workspaceToImport._id)),
);
const nextFolder = (parentId: string) => {
const children = parsed.resources.filter((r: any) => r.parentId === parentId);
let sortPriority = 0;
for (const child of children) {
if (isRequestGroup(child)) {
resources.folders.push(importFolder(child, workspaceToImport._id));
nextFolder(child._id);
} else if (isHttpRequest(child)) {
resources.httpRequests.push(
importHttpRequest(child, workspaceToImport._id, sortPriority++),
);
} else if (isGrpcRequest(child)) {
resources.grpcRequests.push(
importGrpcRequest(child, workspaceToImport._id, sortPriority++),
);
}
}
};
// Import folders
nextFolder(workspaceToImport._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 importEnvironment(e: any, workspaceId: string): ExportResources['environments'][0] {
return {
id: convertId(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}
function importFolder(f: any, workspaceId: string): ExportResources['folders'][0] {
return {
id: convertId(f._id),
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
workspaceId: convertId(workspaceId),
model: 'folder',
name: f.name,
};
}
function importGrpcRequest(
r: any,
workspaceId: string,
sortPriority = 0,
): ExportResources['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._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'grpc_request',
sortPriority,
name: r.name,
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 importHttpRequest(
r: any,
workspaceId: string,
sortPriority = 0,
): ExportResources['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._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'http_request',
sortPriority,
name: r.name,
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 parseVariables(data: Record<string, string>) {
return Object.entries(data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
}));
}
function convertSyntax(variable: string): string {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}
function isWorkspace(obj: any) {
return isJSObject(obj) && obj._type === 'workspace';
}
function isRequestGroup(obj: any) {
return isJSObject(obj) && obj._type === 'request_group';
}
function isHttpRequest(obj: any) {
return isJSObject(obj) && obj._type === 'request';
}
function isGrpcRequest(obj: any) {
return isJSObject(obj) && obj._type === 'grpc_request';
}
function isEnvironment(obj: any) {
return isJSObject(obj) && obj._type === 'environment';
}
function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isJSString(obj: any) {
return Object.prototype.toString.call(obj) === '[object String]';
}
function convertId(id: string): string {
if (id.startsWith('GENERATE_ID::')) {
return id;
}
return `GENERATE_ID::${id}`;
}

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

1505
plugins/importer-postman/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"name": "importer-postman",
"version": "0.0.1",
"devDependencies": {
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,253 @@
import { Environment, Folder, HttpRequest, Model, Workspace } from '../../../src-web/lib/models';
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 function pluginHookImport(
_ctx: any,
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 || '',
variables:
root.variable?.map((v: any) => ({
name: v.key,
value: v.value,
})) ?? [],
};
exportResources.workspaces.push(workspace);
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 request: ExportResources['httpRequests'][0] = {
model: 'http_request',
id: generateId('http_request'),
workspaceId: workspace.id,
folderId,
name: v.name,
method: r.method || 'GET',
url: typeof r.url === 'string' ? r.url : toRecord(r.url).raw,
body: bodyPatch.body,
bodyType: bodyPatch.bodyType,
authentication: authPatch.authentication,
authenticationType: authPatch.authenticationType,
headers: [
...bodyPatch.headers,
...authPatch.headers,
...toArray(r.header).map((h) => {
return {
name: h.key,
value: h.value,
enabled: !h.disabled,
};
}),
],
};
exportResources.httpRequests.push(request);
} else {
console.log('Unknown item', v, folderId);
}
};
for (const item of root.item) {
importItem(item);
}
return { resources: convertTemplateSyntax(exportResources) };
}
function importAuth(
rawAuth: any,
): Pick<HttpRequest, 'authentication' | 'authenticationType' | 'headers'> {
const auth = toRecord(rawAuth);
if ('basic' in auth) {
return {
headers: [],
authenticationType: 'basic',
authentication: {
username: auth.basic.username || '',
password: auth.basic.password || '',
},
};
} else if ('bearer' in auth) {
return {
headers: [],
authenticationType: 'bearer',
authentication: {
token: auth.bearer.token || '',
},
};
} else {
return { headers: [], authenticationType: null, authentication: {} };
}
}
function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
const body = toRecord(rawBody);
if ('graphql' in body) {
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 ('urlencoded' in body) {
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 ('formdata' in body) {
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 ('raw' in body) {
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 {
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;
}
}
const idCount: Partial<Record<Model['model'], number>> = {};
function generateId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View 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"
}
}
]
}

View File

@@ -0,0 +1,91 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { Model } from '../../../src-web/lib/models';
import { pluginHookImport } from '../src';
let originalRandom = Math.random;
describe('importer-postman', () => {
beforeEach(() => {
let i = 0;
// Psuedo-random number generator to ensure consistent ID generation
Math.random = vi.fn(() => ((i++ * 1000) % 133) / 100);
});
afterEach(() => {
Math.random = originalRandom;
});
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const imported = pluginHookImport({}, contents);
const folder0 = newId('folder');
const folder1 = newId('folder');
expect(imported).toEqual({
resources: expect.objectContaining({
workspaces: [
expect.objectContaining({
id: newId('workspace'),
model: 'workspace',
name: 'New Collection',
}),
],
folders: expect.arrayContaining([
expect.objectContaining({
id: folder0,
model: 'folder',
workspaceId: existingId('workspace'),
name: 'Top Folder',
}),
expect.objectContaining({
folderId: folder0,
id: folder1,
model: 'folder',
workspaceId: existingId('workspace'),
name: 'Nested Folder',
}),
]),
httpRequests: expect.arrayContaining([
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 1',
workspaceId: existingId('workspace'),
folderId: folder1,
}),
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 2',
workspaceId: existingId('workspace'),
folderId: folder0,
}),
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 3',
workspaceId: existingId('workspace'),
folderId: null,
}),
]),
}),
});
});
}
});
const idCount: Partial<Record<Model['model'], number>> = {};
function newId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
function existingId(model: Model['model']): string {
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model] ?? 0}`;
}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ESNext",
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"./src"
]
}

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});

12
plugins/importer-yaak/package-lock.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "importer-yaak",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "importer-yaak",
"version": "0.0.1"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "importer-yaak",
"version": "0.0.1"
}

View File

@@ -0,0 +1,29 @@
export function pluginHookImport(ctx: any, 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'];
}
return { resources: parsed.resources }; // Should already be in the correct format
}
export function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}

View File

@@ -0,0 +1,32 @@
import { describe, expect, test } from 'vitest';
import { pluginHookImport } from '../src';
const ctx = {};
describe('importer-yaak', () => {
test('Skips invalid imports', () => {
expect(pluginHookImport(ctx, 'not JSON')).toBeUndefined();
expect(pluginHookImport(ctx, '[]')).toBeUndefined();
expect(pluginHookImport(ctx, JSON.stringify({ resources: {} }))).toBeUndefined();
});
test('converts schema 1 to 2', () => {
const imported = pluginHookImport(
ctx,
JSON.stringify({
yaakSchema: 1,
resources: {
requests: [],
},
}),
);
expect(imported).toEqual(
expect.objectContaining({
resources: {
httpRequests: [],
},
}),
);
});
});

View File

@@ -0,0 +1,15 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
},
});