diff --git a/package.json b/package.json index 229767b1..2d31c77f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "build:plugin:importer-insomnia": "cd plugins/importer-insomnia && vite build --emptyOutDir", "build:plugin:importer-postman": "cd plugins/importer-postman && vite build --emptyOutDir", "build:plugin:importer-yaak": "cd plugins/importer-yaak && vite build --emptyOutDir", + "build:plugin:importer-curl": "cd plugins/importer-curl && vite build --emptyOutDir", "build:plugin:filter-jsonpath": "cd plugins/filter-jsonpath && vite build --emptyOutDir", "build:plugin:filter-xpath": "cd plugins/filter-xpath && vite build --emptyOutDir", "test": "vitest", diff --git a/plugins/importer-curl/package-lock.json b/plugins/importer-curl/package-lock.json new file mode 100644 index 00000000..e9c25e04 --- /dev/null +++ b/plugins/importer-curl/package-lock.json @@ -0,0 +1,243 @@ +{ + "name": "importer-curl", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "importer-curl", + "version": "0.0.1", + "dependencies": { + "shell-quote": "^1.8.1", + "url": "^0.11.3" + }, + "devDependencies": { + "@types/shell-quote": "^1.7.5", + "random-seedable": "^1.0.8" + } + }, + "node_modules/@types/shell-quote": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", + "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-seedable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/random-seedable/-/random-seedable-1.0.8.tgz", + "integrity": "sha512-f6gzvNhAnZBht1Prn0e/tpukUNhkANntFF42uIdWDPriyEATYaRpyH8A9bYaGecUB3AL+dXeYtBUggy18fe3rw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + } + } +} diff --git a/plugins/importer-curl/package.json b/plugins/importer-curl/package.json new file mode 100644 index 00000000..8d32a017 --- /dev/null +++ b/plugins/importer-curl/package.json @@ -0,0 +1,12 @@ +{ + "name": "importer-curl", + "version": "0.0.1", + "dependencies": { + "shell-quote": "^1.8.1", + "url": "^0.11.3" + }, + "devDependencies": { + "@types/shell-quote": "^1.7.5", + "random-seedable": "^1.0.8" + } +} diff --git a/plugins/importer-curl/src/index.ts b/plugins/importer-curl/src/index.ts new file mode 100644 index 00000000..e6346a69 --- /dev/null +++ b/plugins/importer-curl/src/index.ts @@ -0,0 +1,405 @@ +import { ControlOperator, parse, ParseEntry } from 'shell-quote'; +import { + Environment, + Folder, + HttpRequest, + HttpUrlParameter, + Model, + Workspace, +} from '../../../src-web/lib/models'; + +type AtLeast = Partial & Pick; + +interface ExportResources { + workspaces: AtLeast[]; + environments: AtLeast[]; + httpRequests: AtLeast[]; + folders: AtLeast[]; +} + +export const id = 'curl'; +export const name = 'cURL'; +export const description = 'cURL command line tool'; + +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; + +export const pluginHookImport = (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(/([^\\])\n/g, '$1; '); + + 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], + }, + }; +}; + +export 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]; + // trim leading spaces between parsed entries + // regex won't match otherwise (e.g. -H 'Content-Type: application/json') + 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] ?? '' }; + }) ?? []; + + 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: '', + }; + } + return { + name: (name ?? '').trim(), + value: value.trim(), + }; + }); + + // 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, + }); + } + + ///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 = { + params: dataParameters.map((parameter) => ({ + ...parameter, + name: decodeURIComponent(parameter.name || ''), + value: decodeURIComponent(parameter.value || ''), + })), + }; + } 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, + }; + } + + // Method + let method = getPairValue(pairsByName, '', ['X', 'request']).toUpperCase(); + + if (method === '' && body) { + method = 'text' in body || 'params' 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; +} + +const pairsToDataParameters = (keyedPairs: PairsByName) => { + let dataParameters: { + name: string; + value: string; + contentType?: string; + filePath?: string; + }[] = []; + + 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), + }); + } else { + dataParameters.push({ + name: name ?? '', + value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '', + }); + } + } + } + + return dataParameters; +}; + +const getPairValue = ( + 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> = {}; +function generateId(model: Model['model']): string { + idCount[model] = (idCount[model] ?? -1) + 1; + return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`; +} diff --git a/plugins/importer-curl/tests/index.test.ts b/plugins/importer-curl/tests/index.test.ts new file mode 100644 index 00000000..f8574e12 --- /dev/null +++ b/plugins/importer-curl/tests/index.test.ts @@ -0,0 +1,314 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { HttpRequest, Model, Workspace } from '../../../src-web/lib/models'; +import { pluginHookImport } from '../src'; +import { XORShift } from 'random-seedable'; + +let originalRandom = Math.random; + +describe('importer-curl', () => { + beforeEach(() => { + const rand = new XORShift(123456789); + Math.random = vi.fn(() => { + return rand.float(); + }); + }); + + afterEach(() => { + Math.random = originalRandom; + }); + + test('Imports basic GET', () => { + expect(pluginHookImport('curl https://yaak.app')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + }), + ], + }, + }); + }); + + test('Explicit URL', () => { + expect(pluginHookImport('curl --url https://yaak.app')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + }), + ], + }, + }); + }); + + test('Missing URL', () => { + expect(pluginHookImport('curl -X POST')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + method: 'POST', + }), + ], + }, + }); + }); + + test('URL between', () => { + expect(pluginHookImport('curl -v https://yaak.app -X POST')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + method: 'POST', + }), + ], + }, + }); + }); + + test('Random flags', () => { + expect(pluginHookImport('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('curl --request POST https://yaak.app')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + method: 'POST', + }), + ], + }, + }); + }); + + test('Imports -XPOST method', () => { + expect(pluginHookImport('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('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('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', + 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('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', + body: { + params: [ + { name: 'a', value: '' }, + { name: 'b', value: '' }, + { name: 'c', value: 'ccc' }, + ], + }, + }), + ], + }, + }); + }); + + test('Imports data params as text', () => { + expect( + pluginHookImport('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' }], + bodyType: 'text/plain', + body: { text: 'a&b&c=ccc' }, + }), + ], + }, + }); + }); + + test('Imports multiple headers', () => { + expect( + pluginHookImport('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: '' }, + { name: 'Foo', value: 'bar' }, + { name: 'AAA', value: 'bbb' }, + { name: '', value: 'ccc' }, + ], + }), + ], + }, + }); + }); + + test('Imports basic auth', () => { + expect(pluginHookImport('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('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('curl --cookie "foo=bar" https://yaak.app')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + headers: [{ name: 'Cookie', value: 'foo=bar' }], + }), + ], + }, + }); + }); + + test('Imports query params from the URL', () => { + expect(pluginHookImport('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + urlParameters: [ + { name: 'foo', value: 'bar' }, + { name: 'baz', value: 'a%20a' }, + ], + }), + ], + }, + }); + }); +}); + +const idCount: Partial> = {}; + +function baseRequest(mergeWith: Partial) { + 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 = {}) { + idCount.workspace = (idCount.workspace ?? -1) + 1; + return { + id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`, + model: 'workspace', + name: 'Curl Import', + ...mergeWith, + }; +} diff --git a/plugins/importer-curl/vite.config.js b/plugins/importer-curl/vite.config.js new file mode 100644 index 00000000..204e4457 --- /dev/null +++ b/plugins/importer-curl/vite.config.js @@ -0,0 +1,13 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + fileName: 'index', + formats: ['es'], + }, + outDir: resolve(__dirname, '../../src-tauri/plugins/importer-curl'), + }, +}); diff --git a/plugins/importer-insomnia/src/helpers/types.js b/plugins/importer-insomnia/src/helpers/types.js deleted file mode 100644 index d83bfc46..00000000 --- a/plugins/importer-insomnia/src/helpers/types.js +++ /dev/null @@ -1,27 +0,0 @@ -export function isWorkspace(obj) { - return isJSObject(obj) && obj._type === 'workspace'; -} - -export function isRequestGroup(obj) { - return isJSObject(obj) && obj._type === 'request_group'; -} - -export function isHttpRequest(obj) { - return isJSObject(obj) && obj._type === 'request'; -} - -export function isGrpcRequest(obj) { - return isJSObject(obj) && obj._type === 'grpc_request'; -} - -export function isEnvironment(obj) { - return isJSObject(obj) && obj._type === 'environment'; -} - -export function isJSObject(obj) { - return Object.prototype.toString.call(obj) === '[object Object]'; -} - -export function isJSString(obj) { - return Object.prototype.toString.call(obj) === '[object String]'; -} diff --git a/plugins/importer-insomnia/src/helpers/variables.js b/plugins/importer-insomnia/src/helpers/variables.js deleted file mode 100644 index 3104fa42..00000000 --- a/plugins/importer-insomnia/src/helpers/variables.js +++ /dev/null @@ -1,18 +0,0 @@ -import { isJSString } from './types.js'; - -export function parseVariables(data) { - return Object.entries(data).map(([name, value]) => ({ - enabled: true, - name, - value: `${value}`, - })); -} - -/** - * Convert Insomnia syntax to Yaak syntax - * @param {string} variable - Text to convert - */ -export function convertSyntax(variable) { - if (!isJSString(variable)) return variable; - return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}'); -} diff --git a/plugins/importer-insomnia/src/importers/environment.js b/plugins/importer-insomnia/src/importers/environment.js deleted file mode 100644 index 51feaf37..00000000 --- a/plugins/importer-insomnia/src/importers/environment.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Import an Insomnia environment object. - * @param {Object} e - The environment object to import. - * @param workspaceId - Workspace to import into. - */ -export function importEnvironment(e, workspaceId) { - console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2)); - return { - id: e._id, - createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''), - updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''), - workspaceId, - model: 'environment', - name: e.name, - variables: Object.entries(e.data).map(([name, value]) => ({ - enabled: true, - name, - value: `${value}`, - })), - }; -} diff --git a/plugins/importer-insomnia/src/importers/folder.js b/plugins/importer-insomnia/src/importers/folder.js deleted file mode 100644 index 3ae7adda..00000000 --- a/plugins/importer-insomnia/src/importers/folder.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Import an Insomnia folder object. - * @param {Object} f - The environment object to import. - * @param workspaceId - Workspace to import into. - */ -export function importFolder(f, workspaceId) { - console.log('IMPORTING FOLDER', f._id, f.name, JSON.stringify(f, null, 2)); - return { - id: 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 : f.parentId, - workspaceId, - model: 'folder', - name: f.name, - }; -} diff --git a/plugins/importer-insomnia/src/importers/grpcRequest.js b/plugins/importer-insomnia/src/importers/grpcRequest.js deleted file mode 100644 index bdbc9dce..00000000 --- a/plugins/importer-insomnia/src/importers/grpcRequest.js +++ /dev/null @@ -1,37 +0,0 @@ -import { convertSyntax } from '../helpers/variables.js'; - -/** - * Import an Insomnia GRPC request object. - * @param {Object} r - The request object to import. - * @param workspaceId - The workspace ID to use for the request. - * @param {number} sortPriority - The sort priority to use for the request. - */ -export function importGrpcRequest(r, workspaceId, sortPriority = 0) { - console.log('IMPORTING GRPC REQUEST', r._id, r.name, JSON.stringify(r, null, 2)); - - const parts = r.protoMethodName.split('/').filter((p) => p !== ''); - const service = parts[0] ?? null; - const method = parts[1] ?? null; - - return { - id: r._id, - createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''), - updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''), - workspaceId, - folderId: r.parentId === workspaceId ? null : r.parentId, - model: 'grpc_request', - sortPriority, - name: r.name, - url: convertSyntax(r.url), - service, - method, - message: r.body?.text ?? '', - metadata: (r.metadata ?? []) - .map(({ name, value, disabled }) => ({ - enabled: !disabled, - name, - value, - })) - .filter(({ name, value }) => name !== '' || value !== ''), - }; -} diff --git a/plugins/importer-insomnia/src/importers/httpRequest.js b/plugins/importer-insomnia/src/importers/httpRequest.js deleted file mode 100644 index 0bac9926..00000000 --- a/plugins/importer-insomnia/src/importers/httpRequest.js +++ /dev/null @@ -1,60 +0,0 @@ -import { convertSyntax } from '../helpers/variables.js'; - -/** - * Import an Insomnia request object. - * @param {Object} r - The request object to import. - * @param workspaceId - The workspace ID to use for the request. - * @param {number} sortPriority - The sort priority to use for the request. - */ -export function importHttpRequest(r, workspaceId, sortPriority = 0) { - console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2)); - - let bodyType = null; - let body = null; - if (r.body?.mimeType === 'application/graphql') { - bodyType = 'graphql'; - body = convertSyntax(r.body.text); - } else if (r.body?.mimeType === 'application/json') { - bodyType = 'application/json'; - body = convertSyntax(r.body.text); - } - - let authenticationType = 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: r._id, - createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''), - updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''), - workspaceId, - folderId: r.parentId === workspaceId ? null : r.parentId, - model: 'http_request', - sortPriority, - name: r.name, - url: convertSyntax(r.url), - body, - bodyType, - authentication, - authenticationType, - method: r.method, - headers: (r.headers ?? []) - .map(({ name, value, disabled }) => ({ - enabled: !disabled, - name, - value, - })) - .filter(({ name, value }) => name !== '' || value !== ''), - }; -} diff --git a/plugins/importer-insomnia/src/index.js b/plugins/importer-insomnia/src/index.js deleted file mode 100644 index 189dda64..00000000 --- a/plugins/importer-insomnia/src/index.js +++ /dev/null @@ -1,86 +0,0 @@ -import { importEnvironment } from './importers/environment'; -import { importHttpRequest } from './importers/httpRequest'; -import { - isEnvironment, - isJSObject, - isHttpRequest, - isRequestGroup, - isWorkspace, - isGrpcRequest, -} from './helpers/types.js'; -import { parseVariables } from './helpers/variables.js'; -import { importFolder } from './importers/folder.js'; -import { importGrpcRequest } from './importers/grpcRequest'; - -export function pluginHookImport(contents) { - let parsed; - try { - parsed = JSON.parse(contents); - } catch (e) { - return; - } - - if (!isJSObject(parsed)) return; - if (!Array.isArray(parsed.resources)) return; - - const resources = { - workspaces: [], - httpRequests: [], - grpcRequests: [], - environments: [], - folders: [], - }; - - // Import workspaces - const workspacesToImport = parsed.resources.filter(isWorkspace); - for (const workspaceToImport of workspacesToImport) { - const baseEnvironment = parsed.resources.find( - (r) => isEnvironment(r) && r.parentId === workspaceToImport._id, - ); - resources.workspaces.push({ - id: 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) => isEnvironment(r) && r.parentId === baseEnvironment?._id, - ); - resources.environments.push( - ...environmentsToImport.map((r) => importEnvironment(r, workspaceToImport._id)), - ); - - const nextFolder = (parentId) => { - const children = parsed.resources.filter((r) => 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)) { - console.log('GRPC', JSON.stringify(child, null, 1)); - 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 }; -} diff --git a/plugins/importer-insomnia/src/index.ts b/plugins/importer-insomnia/src/index.ts new file mode 100644 index 00000000..74a5c444 --- /dev/null +++ b/plugins/importer-insomnia/src/index.ts @@ -0,0 +1,274 @@ +import { + Environment, + Folder, + GrpcRequest, + HttpRequest, + Workspace, +} from '../../../src-web/lib/models'; + +type AtLeast = Partial & Pick; + +export interface ExportResources { + workspaces: AtLeast[]; + environments: AtLeast[]; + httpRequests: AtLeast[]; + grpcRequests: AtLeast[]; + folders: AtLeast[]; +} + +export function pluginHookImport(contents: string) { + let parsed; + try { + parsed = JSON.parse(contents); + } catch (e) { + return; + } + + 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 = 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 = 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) { + 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}`; +} diff --git a/plugins/importer-insomnia/vite.config.js b/plugins/importer-insomnia/vite.config.js index b7c7bbf6..9d76fdb9 100644 --- a/plugins/importer-insomnia/vite.config.js +++ b/plugins/importer-insomnia/vite.config.js @@ -4,7 +4,7 @@ import { defineConfig } from 'vite'; export default defineConfig({ build: { lib: { - entry: resolve(__dirname, 'src/index.js'), + entry: resolve(__dirname, 'src/index.ts'), fileName: 'index', formats: ['es'], }, diff --git a/plugins/importer-postman/src/index.ts b/plugins/importer-postman/src/index.ts index 6144101d..43af714c 100644 --- a/plugins/importer-postman/src/index.ts +++ b/plugins/importer-postman/src/index.ts @@ -1,4 +1,4 @@ -import { Environment, Folder, HttpRequest, Workspace } from '../../../src-web/lib/models'; +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'; @@ -34,7 +34,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources const workspace: ExportResources['workspaces'][0] = { model: 'workspace', - id: generateId('wk'), + id: generateId('workspace'), name: info.name || 'Postman Import', description: info.description || '', variables: @@ -50,7 +50,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources const folder: ExportResources['folders'][0] = { model: 'folder', workspaceId: workspace.id, - id: generateId('fl'), + id: generateId('folder'), name: v.name, folderId, }; @@ -65,7 +65,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath; const request: ExportResources['httpRequests'][0] = { model: 'http_request', - id: generateId('rq'), + id: generateId('http_request'), workspaceId: workspace.id, folderId, name: v.name, @@ -243,11 +243,8 @@ function convertTemplateSyntax(obj: T): T { } } -export function generateId(prefix: 'wk' | 'rq' | 'fl'): string { - const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - let id = `${prefix}_`; - for (let i = 0; i < 10; i++) { - id += alphabet[Math.floor(Math.random() * alphabet.length)]; - } - return id; +const idCount: Partial> = {}; +function generateId(model: Model['model']): string { + idCount[model] = (idCount[model] ?? -1) + 1; + return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`; } diff --git a/plugins/importer-postman/tests/index.test.ts b/plugins/importer-postman/tests/index.test.ts index e85ecf88..2de5459e 100644 --- a/plugins/importer-postman/tests/index.test.ts +++ b/plugins/importer-postman/tests/index.test.ts @@ -1,6 +1,7 @@ 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; @@ -23,42 +24,67 @@ describe('importer-postman', () => { 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', - workspaceId: 'wk_0G3J6M9QcT', }), expect.objectContaining({ + folderId: folder0, + id: folder1, + model: 'folder', + workspaceId: existingId('workspace'), name: 'Nested Folder', - workspaceId: 'wk_0G3J6M9QcT', }), ]), httpRequests: expect.arrayContaining([ expect.objectContaining({ + id: newId('http_request'), + model: 'http_request', name: 'Request 1', - workspaceId: 'wk_0G3J6M9QcT', - folderId: 'fl_vundefinedyundefinedBundefinedE0H3', + workspaceId: existingId('workspace'), + folderId: folder1, }), expect.objectContaining({ + id: newId('http_request'), + model: 'http_request', name: 'Request 2', - workspaceId: 'wk_0G3J6M9QcT', - folderId: 'fl_fWiZlundefinedoundefinedrundefined', + workspaceId: existingId('workspace'), + folderId: folder0, }), expect.objectContaining({ + id: newId('http_request'), + model: 'http_request', name: 'Request 3', - workspaceId: 'wk_0G3J6M9QcT', + workspaceId: existingId('workspace'), folderId: null, }), ]), - workspaces: [ - expect.objectContaining({ - name: 'New Collection', - }), - ], }), }); }); } }); + +const idCount: Partial> = {}; +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}`; +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5b197e7c..1a254783 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -483,9 +483,9 @@ dependencies = [ [[package]] name = "boa_ast" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73498e9b2f0aa7db74977afa4d594657611e90587abf0dd564c0b55b4a130163" +checksum = "5b6fb81ca0f301f33aff7401e2ffab37dc9e0e4a1cf0ccf6b34f4d9e60aa0682" dependencies = [ "bitflags 2.5.0", "boa_interner", @@ -497,30 +497,35 @@ dependencies = [ [[package]] name = "boa_engine" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16377479d5d6d33896e7acdd1cc698d04a8f72004025bbbddf47558cd29146a6" +checksum = "600e4e4a65b26efcef08a7b1cf2899d3845a32e82e067ee3b75eaf7e413ff31c" dependencies = [ + "arrayvec", "bitflags 2.5.0", "boa_ast", "boa_gc", - "boa_icu_provider", "boa_interner", "boa_macros", "boa_parser", "boa_profiler", - "chrono", + "bytemuck", + "cfg-if", "dashmap", "fast-float", + "hashbrown 0.14.3", "icu_normalizer", "indexmap 2.2.6", - "itertools", + "intrusive-collections", + "itertools 0.12.1", "num-bigint", "num-integer", "num-traits", - "num_enum 0.6.1", + "num_enum 0.7.2", "once_cell", + "paste", "pollster", + "portable-atomic", "rand 0.8.5", "regress", "rustc-hash", @@ -532,39 +537,26 @@ dependencies = [ "tap", "thin-vec", "thiserror", + "time", ] [[package]] name = "boa_gc" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c97b44beaef9d4452342d117d94607fdfa8d474280f1ba0fd97853834e3a49b2" +checksum = "c055ef3cd87ea7db014779195bc90c6adfc35de4902e3b2fe587adecbd384578" dependencies = [ "boa_macros", "boa_profiler", + "hashbrown 0.14.3", "thin-vec", ] -[[package]] -name = "boa_icu_provider" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30e52e34e451dd0bfc2c654a9a43ed34b0073dbd4ae3394b40313edda8627aa" -dependencies = [ - "icu_collections", - "icu_normalizer", - "icu_properties", - "icu_provider", - "icu_provider_adapters", - "icu_provider_blob", - "once_cell", -] - [[package]] name = "boa_interner" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e5afa991908cfbe79bd3109b824e473a1dc5f74f31fab91bb44c9e245daa77" +checksum = "0cacc9caf022d92195c827a3e5bf83f96089d4bfaff834b359ac7b6be46e9187" dependencies = [ "boa_gc", "boa_macros", @@ -578,9 +570,9 @@ dependencies = [ [[package]] name = "boa_macros" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005fa0c5bd20805466dda55eb34cd709bb31a2592bb26927b47714eeed6914d8" +checksum = "6be9c93793b60dac381af475b98634d4b451e28336e72218cad9a20176218dbc" dependencies = [ "proc-macro2", "quote", @@ -590,40 +582,34 @@ dependencies = [ [[package]] name = "boa_parser" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e09afb035377a9044443b598187a7d34cd13164617182a4d7c348522ee3f052" +checksum = "9e8592556849f0619ed142ce2b3a19086769314a8d657f93a5765d06dbce4818" dependencies = [ "bitflags 2.5.0", "boa_ast", - "boa_icu_provider", "boa_interner", "boa_macros", "boa_profiler", "fast-float", - "icu_locid", "icu_properties", - "icu_provider", - "icu_provider_macros", "num-bigint", "num-traits", - "once_cell", "regress", "rustc-hash", - "tinystr", ] [[package]] name = "boa_profiler" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190f92dfe48224adc92881c620f08ccf37ff62b91a094bb357fe53bd5e84647" +checksum = "e0d8372f2d5cbac600a260de87877141b42da1e18d2c7a08ccb493a49cbd55c0" [[package]] name = "boa_runtime" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e7606e8be709e82cb78c66bc6849dfcaa5fd0f551a215bd1cf6ab3b5bb4453" +checksum = "088ac38072a9517691478d33c4385cfd67657ec584b57f8397820a55b660990e" dependencies = [ "boa_engine", "boa_gc", @@ -719,6 +705,20 @@ name = "bytemuck" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] [[package]] name = "byteorder" @@ -903,12 +903,6 @@ dependencies = [ "inout", ] -[[package]] -name = "cobs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" - [[package]] name = "cocoa" version = "0.25.0" @@ -1126,12 +1120,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "critical-section" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" - [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1468,12 +1456,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - [[package]] name = "encoding_rs" version = "0.8.33" @@ -2230,15 +2212,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.11", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -2550,12 +2523,11 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8302d8dfd6044d3ddb3f807a5ef3d7bbca9a574959c6d6e4dc39aa7012d0d5" +checksum = "137d96353afc8544d437e8a99eceb10ab291352699573b0de5b08bda38c78c60" dependencies = [ "displaydoc", - "serde", "yoke", "zerofrom", "zerovec", @@ -2563,29 +2535,48 @@ dependencies = [ [[package]] name = "icu_locid" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3003f85dccfc0e238ff567693248c59153a46f4e6125ba4020b973cef4d1d335" +checksum = "5c0aa2536adc14c07e2a521e95512b75ed8ef832f0fdf9299d4a0a45d2be2a9d" dependencies = [ "displaydoc", "litemap", - "serde", "tinystr", "writeable", "zerovec", ] [[package]] -name = "icu_normalizer" -version = "1.2.0" +name = "icu_locid_transform" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652869735c9fb9f5a64ba180ee16f2c848390469c116deef517ecc53f4343598" +checksum = "57c17d8f6524fdca4471101dd71f0a132eb6382b5d6d7f2970441cb25f6f435a" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c6c3e8bf9580e2dafee8de6f9ec14826aaf359787789c7724f1f85f47d3dc" + +[[package]] +name = "icu_normalizer" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c183e31ed700f1ecd6b032d104c52fe8b15d028956b73727c97ec176b170e187" dependencies = [ "displaydoc", "icu_collections", + "icu_normalizer_data", "icu_properties", "icu_provider", - "serde", "smallvec", "utf16_iter", "utf8_iter", @@ -2594,74 +2585,58 @@ dependencies = [ ] [[package]] -name = "icu_properties" -version = "1.2.0" +name = "icu_normalizer_data" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0e1aa26851f16c9e04412a5911c86b7f8768dac8f8d4c5f1c568a7e5d7a434" +checksum = "22026918a80e6a9a330cb01b60f950e2b4e5284c59528fd0c6150076ef4c8522" + +[[package]] +name = "icu_properties" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976e296217453af983efa25f287a4c1da04b9a63bf1ed63719455068e4453eb5" dependencies = [ "displaydoc", "icu_collections", + "icu_locid_transform", + "icu_properties_data", "icu_provider", - "serde", "tinystr", "zerovec", ] [[package]] -name = "icu_provider" -version = "1.2.0" +name = "icu_properties_data" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc312a7b6148f7dfe098047ae2494d12d4034f48ade58d4f353000db376e305" +checksum = "f6a86c0e384532b06b6c104814f9c1b13bcd5b64409001c0d05713a1f3529d99" + +[[package]] +name = "icu_provider" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba58e782287eb6950247abbf11719f83f5d4e4a5c1f2cd490d30a334bc47c2f4" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", - "postcard", - "serde", "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] -[[package]] -name = "icu_provider_adapters" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ae1e2bd0c41728b77e7c46e9afdec5e2127d1eedacc684724667d50c126bd3" -dependencies = [ - "icu_locid", - "icu_provider", - "serde", - "tinystr", - "yoke", - "zerovec", -] - -[[package]] -name = "icu_provider_blob" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd364c9a01f791a4bc04a74cf2a1d01d9f6926a40fd5ae1c28004e1e70d8338b" -dependencies = [ - "icu_provider", - "postcard", - "serde", - "writeable", - "yoke", - "zerovec", -] - [[package]] name = "icu_provider_macros" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b728b9421e93eff1d9f8681101b78fa745e0748c95c655c83f337044a7e10" +checksum = "d2abdd3a62551e8337af119c5899e600ca0c88ec8f23a46c60ba216c803dcf1a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] @@ -2750,6 +2725,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intrusive-collections" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b694dc9f70c3bda874626d2aed13b780f137aab435f4e9814121955cf706122e" +dependencies = [ + "memoffset", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2804,6 +2788,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -3396,11 +3389,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.6.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ - "num_enum_derive 0.6.1", + "num_enum_derive 0.7.2", ] [[package]] @@ -3417,11 +3410,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.6.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 2.0.58", @@ -3489,10 +3482,6 @@ name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "open" @@ -3976,17 +3965,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" -[[package]] -name = "postcard" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" -dependencies = [ - "cobs", - "embedded-io", - "serde", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -4081,7 +4059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.58", @@ -4406,11 +4384,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "regress" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9ecfa0cb04d0b04dddb99b8ccf4f66bc8dfd23df694b398570bd8ae3a50fb" +checksum = "0eae2a1ebfecc58aff952ef8ccd364329abe627762f5bf09ff42eb9d98522479" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.3", "memchr", ] @@ -4756,9 +4734,9 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "ryu-js" -version = "0.2.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" +checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" [[package]] name = "safemem" @@ -5233,7 +5211,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ - "itertools", + "itertools 0.11.0", "nom 7.1.3", "unicode_categories", ] @@ -6114,6 +6092,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa 1.0.9", + "js-sys", "libc", "num-conv", "num_threads", @@ -6141,12 +6120,11 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8faa444297615a4e020acb64146b0603c9c395c03a97c17fd9028816d3b4d63e" +checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" dependencies = [ "displaydoc", - "serde", "zerovec", ] @@ -7615,11 +7593,10 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zerovec" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591691014119b87047ead4dcf3e6adfbf73cb7c38ab6980d4f18a32138f35d46" +checksum = "eff4439ae91fb5c72b8abc12f3f2dbf51bd27e6eadb9f8a5bc8898dddb0e27ea" dependencies = [ - "serde", "yoke", "zerofrom", "zerovec-derive", @@ -7627,9 +7604,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4a1638a1934450809c2266a70362bfc96cd90550c073f5b8a55014d1010157" +checksum = "7b4e5997cbf58990550ef1f0e5124a05e47e1ebd33a84af25739be6031a62c20" dependencies = [ "proc-macro2", "quote", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2e2d2e49..d764f9ac 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,8 +29,8 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa [dependencies] base64 = "0.22.0" -boa_engine = { version = "0.17.3", features = ["annex-b"] } -boa_runtime = { version = "0.17.3" } +boa_engine = { version = "0.18.0", features = ["annex-b"] } +boa_runtime = { version = "0.18.0" } chrono = { version = "0.4.31", features = ["serde"] } futures = "0.3.26" http = "0.2.8" diff --git a/src-tauri/plugins/importer-curl/index.mjs b/src-tauri/plugins/importer-curl/index.mjs new file mode 100644 index 00000000..b23a94e6 --- /dev/null +++ b/src-tauri/plugins/importer-curl/index.mjs @@ -0,0 +1,283 @@ +var j = "(?:" + [ + "\\|\\|", + "\\&\\&", + ";;", + "\\|\\&", + "\\<\\(", + "\\<\\<\\<", + ">>", + ">\\&", + "<\\&", + "[&;()|<>]" +].join("|") + ")", D = new RegExp("^" + j + "$"), q = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, _ = "'", G = '"', U = "$", R = "", z = 4294967296; +for (var L = 0; L < 4; L++) + R += (z * Math.random()).toString(16); +var J = new RegExp("^" + R); +function X(n, s) { + for (var e = s.lastIndex, t = [], c; c = s.exec(n); ) + t.push(c), s.lastIndex === c.index && (s.lastIndex += 1); + return s.lastIndex = e, t; +} +function F(n, s, e) { + var t = typeof n == "function" ? n(e) : n[e]; + return typeof t > "u" && e != "" ? t = "" : typeof t > "u" && (t = "$"), typeof t == "object" ? s + R + JSON.stringify(t) + R : s + t; +} +function K(n, s, e) { + e || (e = {}); + var t = e.escape || "\\", c = "(\\" + t + `['"` + q + `]|[^\\s'"` + q + "])+", m = new RegExp([ + "(" + j + ")", + // control chars + "(" + c + "|" + M + "|" + Q + ")+" + ].join("|"), "g"), f = X(n, m); + if (f.length === 0) + return []; + s || (s = {}); + var w = !1; + return f.map(function(r) { + var a = r[0]; + if (!a || w) + return; + if (D.test(a)) + return { op: a }; + var x = !1, O = !1, p = "", A = !1, i; + function b() { + i += 1; + var v, d, T = a.charAt(i); + if (T === "{") { + if (i += 1, a.charAt(i) === "}") + throw new Error("Bad substitution: " + a.slice(i - 2, i + 1)); + if (v = a.indexOf("}", i), v < 0) + throw new Error("Bad substitution: " + a.slice(i)); + d = a.slice(i, v), i = v; + } else if (/[*@#?$!_-]/.test(T)) + d = T, i += 1; + else { + var g = a.slice(i); + v = g.match(/[^\w\d_]/), v ? (d = g.slice(0, v.index), i += v.index - 1) : (d = g, i = a.length); + } + return F(s, "", d); + } + for (i = 0; i < a.length; i++) { + var u = a.charAt(i); + if (A = A || !x && (u === "*" || u === "?"), O) + p += u, O = !1; + else if (x) + u === x ? x = !1 : x == _ ? p += u : u === t ? (i += 1, u = a.charAt(i), u === G || u === t || u === U ? p += u : p += t + u) : u === U ? p += b() : p += u; + else if (u === G || u === _) + x = u; + else { + if (D.test(u)) + return { op: a }; + if (V.test(u)) { + w = !0; + var E = { comment: n.slice(r.index + i + 1) }; + return p.length ? [p, E] : [E]; + } else + u === t ? O = !0 : u === U ? p += b() : p += u; + } + } + return A ? { op: "glob", pattern: p } : p; + }).reduce(function(r, a) { + return typeof a > "u" ? r : r.concat(a); + }, []); +} +var Y = function(s, e, t) { + var c = K(s, e, t); + return typeof e != "function" ? c : c.reduce(function(m, f) { + if (typeof f == "object") + return m.concat(f); + var w = f.split(RegExp("(" + R + ".*?" + R + ")", "g")); + return w.length === 1 ? m.concat(w[0]) : m.concat(w.filter(Boolean).map(function(r) { + return J.test(r) ? JSON.parse(r.split(R)[1]) : r; + })); + }, []); +}, Z = Y; +const ae = "curl", se = "cURL", ie = "cURL command line tool", H = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"], ee = [ + ["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 + H +].flatMap((n) => n), oe = (n) => { + if (!n.match(/^\s*curl /)) + return null; + const s = [], e = n.replace(/([^\\])\n/g, "$1; "); + let t = []; + const m = Z(e).flatMap((r) => typeof r == "string" && r.startsWith("-") && !r.startsWith("--") && r.length > 2 ? [r.slice(0, 2), r.slice(2)] : r); + for (const r of m) { + if (typeof r == "string") { + r.startsWith("$") ? t.push(r.slice(1)) : t.push(r); + continue; + } + if ("comment" in r) + continue; + const { op: a } = r; + if (a === ";") { + s.push(t), t = []; + continue; + } + if (a != null && a.startsWith("$")) { + const x = a.slice(2, a.length - 1).replace(/\\'/g, "'"); + t.push(x); + continue; + } + a === "glob" && t.push(r.pattern); + } + s.push(t); + const f = { + model: "workspace", + id: N("workspace"), + name: "Curl Import" + }; + return { + resources: { + httpRequests: s.filter((r) => r[0] === "curl").map((r) => te(r, f.id)), + workspaces: [f] + } + }; +}; +function te(n, s) { + const e = {}, t = []; + for (let o = 1; o < n.length; o++) { + let l = n[o]; + if (typeof l == "string" && (l = l.trim()), typeof l == "string" && l.match(/^-{1,2}[\w-]+/)) { + const $ = l[0] === "-" && l[1] !== "-"; + let h = l.replace(/^-{1,2}/, ""); + if (!ee.includes(h)) + continue; + let y; + const S = n[o + 1]; + $ && h.length > 1 ? (y = h.slice(1), h = h.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, o++) : y = !0, e[h] = e[h] || [], e[h].push(y); + } else + l && t.push(l); + } + let c, m; + const f = C(e, t[0] || "", ["url"]), [w, r] = W(f, "?"); + c = (r == null ? void 0 : r.split("&").map((o) => { + const l = W(o, "="); + return { name: l[0] ?? "", value: l[1] ?? "" }; + })) ?? [], m = w ?? f; + const [a, x] = C(e, "", ["u", "user"]).split(/:(.*)$/), O = C(e, !1, ["digest"]), p = a ? O ? "digest" : "basic" : null, A = a ? { + username: a.trim(), + password: (x ?? "").trim() + } : {}, i = [ + ...e.header || [], + ...e.H || [] + ].map((o) => { + const [l, $] = o.split(/:(.*)$/); + return $ ? { + name: (l ?? "").trim(), + value: $.trim() + } : { + name: (l ?? "").trim().replace(/;$/, ""), + value: "" + }; + }), b = [ + ...e.cookie || [], + ...e.b || [] + ].map((o) => { + const l = o.split("=", 1)[0], $ = o.replace(`${l}=`, ""); + return `${l}=${$}`; + }).join("; "), u = i.find((o) => o.name.toLowerCase() === "cookie"); + b && u ? u.value += `; ${b}` : b && i.push({ + name: "Cookie", + value: b + }); + const E = ne(e), v = i.find((o) => o.name.toLowerCase() === "content-type"), d = v ? v.value.split(";")[0] : null, T = [ + ...e.form || [], + ...e.F || [] + ].map((o) => { + const l = o.split("="), $ = l[0] ?? "", h = l[1] ?? "", y = { + name: $, + enabled: !0 + }; + return h.indexOf("@") === 0 ? y.file = h.slice(1) : y.value = h, y; + }); + let g = {}, I = null; + const B = C(e, !1, ["G", "get"]); + E.length > 0 && B ? c.push(...E) : E.length > 0 && (d == null || d === "application/x-www-form-urlencoded") ? (I = d ?? "application/x-www-form-urlencoded", g = { + params: E.map((o) => ({ + ...o, + name: decodeURIComponent(o.name || ""), + value: decodeURIComponent(o.value || "") + })) + }) : E.length > 0 ? (I = d === "application/json" || d === "text/xml" || d === "text/plain" ? d : "other", g = { + text: E.map(({ name: o, value: l }) => o && l ? `${o}=${l}` : o || l).join("&") + }) : T.length && (I = d ?? "multipart/form-data", g = { + form: T + }); + let P = C(e, "", ["X", "request"]).toUpperCase(); + return P === "" && g && (P = "text" in g || "params" in g ? "POST" : "GET"), { + id: N("http_request"), + model: "http_request", + workspaceId: s, + name: "", + urlParameters: c, + url: m, + method: P, + headers: i, + authentication: A, + authenticationType: p, + body: g, + bodyType: I, + folderId: null, + sortPriority: 0 + }; +} +const ne = (n) => { + let s = []; + for (const e of H) { + const t = n[e]; + if (!(!t || t.length === 0)) + for (const c of t) { + if (typeof c != "string") + continue; + const [m, f] = c.split("="); + c.startsWith("@") ? s.push({ + name: m ?? "", + value: "", + filePath: c.slice(1) + }) : s.push({ + name: m ?? "", + value: e === "data-urlencode" ? encodeURIComponent(f ?? "") : f ?? "" + }); + } + } + return s; +}, C = (n, s, e) => { + for (const t of e) + if (n[t] && n[t].length) + return n[t][0]; + return s; +}; +function W(n, s) { + const e = n.indexOf(s); + return e > -1 ? [n.slice(0, e), n.slice(e + 1)] : [n]; +} +const k = {}; +function N(n) { + return k[n] = (k[n] ?? -1) + 1, `GENERATE_ID::${n.toUpperCase()}_${k[n]}`; +} +export { + ie as description, + ae as id, + te as importCommand, + se as name, + oe as pluginHookImport +}; diff --git a/src-tauri/plugins/importer-insomnia/index.mjs b/src-tauri/plugins/importer-insomnia/index.mjs index 548d2240..d06fd5ef 100644 --- a/src-tauri/plugins/importer-insomnia/index.mjs +++ b/src-tauri/plugins/importer-insomnia/index.mjs @@ -1,165 +1,179 @@ -function g(e, n) { - return console.log("IMPORTING Environment", e._id, e.name, JSON.stringify(e, null, 2)), { - id: e._id, - createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), - updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), - workspaceId: n, - model: "environment", - name: e.name, - variables: Object.entries(e.data).map(([t, a]) => ({ - enabled: !0, - name: t, - value: `${a}` - })) - }; -} -function S(e) { - return m(e) && e._type === "workspace"; -} -function I(e) { - return m(e) && e._type === "request_group"; -} -function y(e) { - return m(e) && e._type === "request"; -} -function h(e) { - return m(e) && e._type === "grpc_request"; -} -function f(e) { - return m(e) && e._type === "environment"; -} -function m(e) { - return Object.prototype.toString.call(e) === "[object Object]"; -} -function w(e) { - return Object.prototype.toString.call(e) === "[object String]"; -} -function O(e) { - return Object.entries(e).map(([n, t]) => ({ - enabled: !0, - name: n, - value: `${t}` - })); -} -function d(e) { - return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e; -} -function _(e, n, t = 0) { - var l, r; - console.log("IMPORTING REQUEST", e._id, e.name, JSON.stringify(e, null, 2)); - let a = null, o = null; - ((l = e.body) == null ? void 0 : l.mimeType) === "application/graphql" ? (a = "graphql", o = d(e.body.text)) : ((r = e.body) == null ? void 0 : r.mimeType) === "application/json" && (a = "application/json", o = d(e.body.text)); - let s = null, p = {}; - return e.authentication.type === "bearer" ? (s = "bearer", p = { - token: d(e.authentication.token) - }) : e.authentication.type === "basic" && (s = "basic", p = { - username: d(e.authentication.username), - password: d(e.authentication.password) - }), { - id: e._id, - createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), - updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), - workspaceId: n, - folderId: e.parentId === n ? null : e.parentId, - model: "http_request", - sortPriority: t, - name: e.name, - url: d(e.url), - body: o, - bodyType: a, - authentication: p, - authenticationType: s, - method: e.method, - headers: (e.headers ?? []).map(({ name: u, value: c, disabled: i }) => ({ - enabled: !i, - name: u, - value: c - })).filter(({ name: u, value: c }) => u !== "" || c !== "") - }; -} -function R(e, n) { - return console.log("IMPORTING FOLDER", e._id, e.name, JSON.stringify(e, null, 2)), { - id: e._id, - createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), - updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), - folderId: e.parentId === n ? null : e.parentId, - workspaceId: n, - model: "folder", - name: e.name - }; -} -function D(e, n, t = 0) { - var p; - console.log("IMPORTING GRPC REQUEST", e._id, e.name, JSON.stringify(e, null, 2)); - const a = e.protoMethodName.split("/").filter((l) => l !== ""), o = a[0] ?? null, s = a[1] ?? null; - return { - id: e._id, - createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), - updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), - workspaceId: n, - folderId: e.parentId === n ? null : e.parentId, - model: "grpc_request", - sortPriority: t, - name: e.name, - url: d(e.url), - service: o, - method: s, - message: ((p = e.body) == null ? void 0 : p.text) ?? "", - metadata: (e.metadata ?? []).map(({ name: l, value: r, disabled: u }) => ({ - enabled: !u, - name: l, - value: r - })).filter(({ name: l, value: r }) => l !== "" || r !== "") - }; -} -function q(e) { - let n; +function A(e) { + let a; try { - n = JSON.parse(e); + a = JSON.parse(e); } catch { return; } - if (!m(n) || !Array.isArray(n.resources)) + if (!c(a) || !Array.isArray(a.resources)) return; - const t = { + const n = { workspaces: [], httpRequests: [], grpcRequests: [], environments: [], folders: [] - }, a = n.resources.filter(S); - for (const o of a) { - const s = n.resources.find( - (r) => f(r) && r.parentId === o._id + }, o = a.resources.filter(_); + for (const r of o) { + const l = a.resources.find( + (i) => w(i) && i.parentId === r._id ); - t.workspaces.push({ - id: o._id, - createdAt: new Date(a.created ?? Date.now()).toISOString().replace("Z", ""), - updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace("Z", ""), + n.workspaces.push({ + id: d(r._id), + createdAt: new Date(o.created ?? Date.now()).toISOString().replace("Z", ""), + updatedAt: new Date(o.updated ?? Date.now()).toISOString().replace("Z", ""), model: "workspace", - name: o.name, - variables: s ? O(s.data) : [] + name: r.name, + variables: l ? h(l.data) : [] }); - const p = n.resources.filter( - (r) => f(r) && r.parentId === (s == null ? void 0 : s._id) + const p = a.resources.filter( + (i) => w(i) && i.parentId === (l == null ? void 0 : l._id) ); - t.environments.push( - ...p.map((r) => g(r, o._id)) + n.environments.push( + ...p.map((i) => b(i, r._id)) ); - const l = (r) => { - const u = n.resources.filter((i) => i.parentId === r); - let c = 0; - for (const i of u) - I(i) ? (t.folders.push(R(i, o._id)), l(i._id)) : y(i) ? t.httpRequests.push( - _(i, o._id, c++) - ) : h(i) && (console.log("GRPC", JSON.stringify(i, null, 1)), t.grpcRequests.push( - D(i, o._id, c++) - )); + const s = (i) => { + const f = a.resources.filter((t) => t.parentId === i); + let m = 0; + for (const t of f) + D(t) ? (n.folders.push(I(t, r._id)), s(t._id)) : v(t) ? n.httpRequests.push( + g(t, r._id, m++) + ) : q(t) && n.grpcRequests.push( + S(t, r._id, m++) + ); }; - l(o._id); + s(r._id); } - return t.httpRequests = t.httpRequests.filter(Boolean), t.grpcRequests = t.grpcRequests.filter(Boolean), t.environments = t.environments.filter(Boolean), t.workspaces = t.workspaces.filter(Boolean), { resources: t }; + return n.httpRequests = n.httpRequests.filter(Boolean), n.grpcRequests = n.grpcRequests.filter(Boolean), n.environments = n.environments.filter(Boolean), n.workspaces = n.workspaces.filter(Boolean), { resources: n }; +} +function b(e, a) { + return { + id: d(e._id), + createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), + updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), + workspaceId: d(a), + model: "environment", + name: e.name, + variables: Object.entries(e.data).map(([n, o]) => ({ + enabled: !0, + name: n, + value: `${o}` + })) + }; +} +function I(e, a) { + return { + id: d(e._id), + createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), + updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), + folderId: e.parentId === a ? null : d(e.parentId), + workspaceId: d(a), + model: "folder", + name: e.name + }; +} +function S(e, a, n = 0) { + var p; + const o = e.protoMethodName.split("/").filter((s) => s !== ""), r = o[0] ?? null, l = o[1] ?? null; + return { + id: d(e._id), + createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), + updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), + workspaceId: d(a), + folderId: e.parentId === a ? null : d(e.parentId), + model: "grpc_request", + sortPriority: n, + name: e.name, + url: u(e.url), + service: r, + method: l, + message: ((p = e.body) == null ? void 0 : p.text) ?? "", + metadata: (e.metadata ?? []).map((s) => ({ + enabled: !s.disabled, + name: s.name ?? "", + value: s.value ?? "" + })).filter(({ name: s, value: i }) => s !== "" || i !== "") + }; +} +function g(e, a, n = 0) { + var s, i, f, m; + let o = null, r = {}; + e.body.mimeType === "application/octet-stream" ? (o = "binary", r = { filePath: e.body.fileName ?? "" }) : ((s = e.body) == null ? void 0 : s.mimeType) === "application/x-www-form-urlencoded" ? (o = "application/x-www-form-urlencoded", r = { + form: (e.body.params ?? []).map((t) => ({ + enabled: !t.disabled, + name: t.name ?? "", + value: t.value ?? "" + })) + }) : ((i = e.body) == null ? void 0 : i.mimeType) === "multipart/form-data" ? (o = "multipart/form-data", r = { + form: (e.body.params ?? []).map((t) => ({ + enabled: !t.disabled, + name: t.name ?? "", + value: t.value ?? "", + file: t.fileName ?? null + })) + }) : ((f = e.body) == null ? void 0 : f.mimeType) === "application/graphql" ? (o = "graphql", r = { text: u(e.body.text ?? "") }) : ((m = e.body) == null ? void 0 : m.mimeType) === "application/json" && (o = "application/json", r = { text: u(e.body.text ?? "") }); + let l = null, p = {}; + return e.authentication.type === "bearer" ? (l = "bearer", p = { + token: u(e.authentication.token) + }) : e.authentication.type === "basic" && (l = "basic", p = { + username: u(e.authentication.username), + password: u(e.authentication.password) + }), { + id: d(e._id), + createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""), + updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""), + workspaceId: d(a), + folderId: e.parentId === a ? null : d(e.parentId), + model: "http_request", + sortPriority: n, + name: e.name, + url: u(e.url), + body: r, + bodyType: o, + authentication: p, + authenticationType: l, + method: e.method, + headers: (e.headers ?? []).map((t) => ({ + enabled: !t.disabled, + name: t.name ?? "", + value: t.value ?? "" + })).filter(({ name: t, value: y }) => t !== "" || y !== "") + }; +} +function h(e) { + return Object.entries(e).map(([a, n]) => ({ + enabled: !0, + name: a, + value: `${n}` + })); +} +function u(e) { + return O(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e; +} +function _(e) { + return c(e) && e._type === "workspace"; +} +function D(e) { + return c(e) && e._type === "request_group"; +} +function v(e) { + return c(e) && e._type === "request"; +} +function q(e) { + return c(e) && e._type === "grpc_request"; +} +function w(e) { + return c(e) && e._type === "environment"; +} +function c(e) { + return Object.prototype.toString.call(e) === "[object Object]"; +} +function O(e) { + return Object.prototype.toString.call(e) === "[object String]"; +} +function d(e) { + return e.startsWith("GENERATE_ID::") ? e : `GENERATE_ID::${e}`; } export { - q as pluginHookImport + A as pluginHookImport }; diff --git a/src-tauri/plugins/importer-postman/index.mjs b/src-tauri/plugins/importer-postman/index.mjs index 6a07464e..fd6d58c9 100644 --- a/src-tauri/plugins/importer-postman/index.mjs +++ b/src-tauri/plugins/importer-postman/index.mjs @@ -1,92 +1,92 @@ -const q = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", S = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", _ = [S, q]; -function j(t) { +const S = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", _ = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", O = [_, S]; +function v(e) { var g; - const e = A(t); - if (e == null) + const t = k(e); + if (t == null) return; - const a = l(e.info); - if (!_.includes(a.schema) || !Array.isArray(e.item)) + const o = i(t.info); + if (!O.includes(o.schema) || !Array.isArray(t.item)) return; - const c = w(e.auth), s = { + const u = A(t.auth), s = { workspaces: [], environments: [], httpRequests: [], folders: [] }, n = { model: "workspace", - id: y("wk"), - name: a.name || "Postman Import", - description: a.description || "", - variables: ((g = e.variable) == null ? void 0 : g.map((r) => ({ + id: h("workspace"), + name: o.name || "Postman Import", + description: o.description || "", + variables: ((g = t.variable) == null ? void 0 : g.map((r) => ({ name: r.key, value: r.value }))) ?? [] }; s.workspaces.push(n); - const f = (r, d = null) => { + const T = (r, p = null) => { if (typeof r.name == "string" && Array.isArray(r.item)) { - const o = { + const a = { model: "folder", workspaceId: n.id, - id: y("fl"), + id: h("folder"), name: r.name, - folderId: d + folderId: p }; - s.folders.push(o); - for (const u of r.item) - f(u, o.id); + s.folders.push(a); + for (const l of r.item) + T(l, a.id); } else if (typeof r.name == "string" && "request" in r) { - const o = l(r.request), u = O(o.body), T = w(o.auth), p = T.authenticationType == null ? c : T, k = { + const a = i(r.request), l = j(a.body), w = A(a.auth), d = w.authenticationType == null ? u : w, q = { model: "http_request", - id: y("rq"), + id: h("http_request"), workspaceId: n.id, - folderId: d, + folderId: p, name: r.name, - method: o.method || "GET", - url: typeof o.url == "string" ? o.url : l(o.url).raw, - body: u.body, - bodyType: u.bodyType, - authentication: p.authentication, - authenticationType: p.authenticationType, + method: a.method || "GET", + url: typeof a.url == "string" ? a.url : i(a.url).raw, + body: l.body, + bodyType: l.bodyType, + authentication: d.authentication, + authenticationType: d.authenticationType, headers: [ - ...u.headers, - ...p.headers, - ...h(o.header).map((m) => ({ + ...l.headers, + ...d.headers, + ...b(a.header).map((m) => ({ name: m.key, value: m.value, enabled: !m.disabled })) ] }; - s.httpRequests.push(k); + s.httpRequests.push(q); } else - console.log("Unknown item", r, d); + console.log("Unknown item", r, p); }; - for (const r of e.item) - f(r); - return { resources: b(s) }; + for (const r of t.item) + T(r); + return { resources: f(s) }; } -function w(t) { - const e = l(t); - return "basic" in e ? { +function A(e) { + const t = i(e); + return "basic" in t ? { headers: [], authenticationType: "basic", authentication: { - username: e.basic.username || "", - password: e.basic.password || "" + username: t.basic.username || "", + password: t.basic.password || "" } - } : "bearer" in e ? { + } : "bearer" in t ? { headers: [], authenticationType: "bearer", authentication: { - token: e.bearer.token || "" + token: t.bearer.token || "" } } : { headers: [], authenticationType: null, authentication: {} }; } -function O(t) { - var a, i, c, s; - const e = l(t); - return "graphql" in e ? { +function j(e) { + var o, c, u, s; + const t = i(e); + return "graphql" in t ? { headers: [ { name: "Content-Type", @@ -97,12 +97,12 @@ function O(t) { bodyType: "graphql", body: { text: JSON.stringify( - { query: e.graphql.query, variables: A(e.graphql.variables) }, + { query: t.graphql.query, variables: k(t.graphql.variables) }, null, 2 ) } - } : "urlencoded" in e ? { + } : "urlencoded" in t ? { headers: [ { name: "Content-Type", @@ -112,13 +112,13 @@ function O(t) { ], bodyType: "application/x-www-form-urlencoded", body: { - form: h(e.urlencoded).map((n) => ({ + form: b(t.urlencoded).map((n) => ({ enabled: !n.disabled, name: n.key ?? "", value: n.value ?? "" })) } - } : "formdata" in e ? { + } : "formdata" in t ? { headers: [ { name: "Content-Type", @@ -128,7 +128,7 @@ function O(t) { ], bodyType: "multipart/form-data", body: { - form: h(e.formdata).map( + form: b(t.formdata).map( (n) => n.src != null ? { enabled: !n.disabled, name: n.key ?? "", @@ -140,46 +140,42 @@ function O(t) { } ) } - } : "raw" in e ? { + } : "raw" in t ? { headers: [ { name: "Content-Type", - value: ((i = (a = e.options) == null ? void 0 : a.raw) == null ? void 0 : i.language) === "json" ? "application/json" : "", + value: ((c = (o = t.options) == null ? void 0 : o.raw) == null ? void 0 : c.language) === "json" ? "application/json" : "", enabled: !0 } ], - bodyType: ((s = (c = e.options) == null ? void 0 : c.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "other", + bodyType: ((s = (u = t.options) == null ? void 0 : u.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "other", body: { - text: e.raw ?? "" + text: t.raw ?? "" } } : { headers: [], bodyType: null, body: {} }; } -function A(t) { +function k(e) { try { - return l(JSON.parse(t)); + return i(JSON.parse(e)); } catch { } return null; } -function l(t) { - return Object.prototype.toString.call(t) === "[object Object]" ? t : {}; +function i(e) { + return Object.prototype.toString.call(e) === "[object Object]" ? e : {}; } -function h(t) { - return Object.prototype.toString.call(t) === "[object Array]" ? t : []; +function b(e) { + return Object.prototype.toString.call(e) === "[object Array]" ? e : []; } -function b(t) { - return typeof t == "string" ? t.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(t) && t != null ? t.map(b) : typeof t == "object" && t != null ? Object.fromEntries( - Object.entries(t).map(([e, a]) => [e, b(a)]) - ) : t; +function f(e) { + return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(f) : typeof e == "object" && e != null ? Object.fromEntries( + Object.entries(e).map(([t, o]) => [t, f(o)]) + ) : e; } -function y(t) { - const e = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - let a = `${t}_`; - for (let i = 0; i < 10; i++) - a += e[Math.floor(Math.random() * e.length)]; - return a; +const y = {}; +function h(e) { + return y[e] = (y[e] ?? -1) + 1, `GENERATE_ID::${e.toUpperCase()}_${y[e]}`; } export { - y as generateId, - j as pluginHookImport + v as pluginHookImport }; diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index 2771d5dd..811865f8 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -233,7 +233,7 @@ fn get_window_size(app_handle: &AppHandle) -> String { async fn get_id(app_handle: &AppHandle) -> String { let id = get_key_value_string(app_handle, "analytics", "id", "").await; if id.is_empty() { - let new_id = generate_id(None); + let new_id = generate_id(); set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await; new_id } else { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 555b79f6..1c9f7ee0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -38,22 +38,7 @@ use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http::send_http_request; -use crate::models::{ - cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, - delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, - delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, - delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, - get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, - get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, - get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, - list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, - list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, - upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, - upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, - Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, - GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, Settings, Workspace, - WorkspaceExportResources, -}; +use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, Settings, Workspace, WorkspaceExportResources, generate_model_id, ModelType}; use crate::plugin::ImportResult; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::window_menu::app_menu; @@ -757,27 +742,59 @@ async fn cmd_import_data( None => Err("No import handlers found".to_string()), Some(r) => { let mut imported_resources = WorkspaceExportResources::default(); + let mut id_map: HashMap = HashMap::new(); + + let maybe_gen_id = |id: &str, model: ModelType, ids: &mut HashMap| -> String { + if !id.starts_with("GENERATE_ID::") { + return id.to_string(); + } + + let unique_key = id.replace("GENERATE_ID", ""); + if let Some(existing) = ids.get(unique_key.as_str()) { + existing.to_string() + } else { + let new_id = generate_model_id(model); + ids.insert(unique_key, new_id.clone()); + new_id + } + }; + + let maybe_gen_id_opt = |id: Option, model: ModelType, ids: &mut HashMap| -> Option { + match id { + Some(id) => Some(maybe_gen_id(id.as_str(), model, ids)), + None => None, + } + }; info!("Importing resources"); - for v in r.resources.workspaces { + for mut v in r.resources.workspaces { + v.id = maybe_gen_id(v.id.as_str(), ModelType::Workspace, &mut id_map); let x = upsert_workspace(&w, v).await.map_err(|e| e.to_string())?; imported_resources.workspaces.push(x.clone()); info!("Imported workspace: {}", x.name); } - for v in r.resources.environments { + for mut v in r.resources.environments { + v.id = maybe_gen_id(v.id.as_str(), ModelType::Environment, &mut id_map); + v.workspace_id = maybe_gen_id(v.workspace_id.as_str(), ModelType::Workspace, &mut id_map); let x = upsert_environment(&w, v).await.map_err(|e| e.to_string())?; imported_resources.environments.push(x.clone()); info!("Imported environment: {}", x.name); } - for v in r.resources.folders { + for mut v in r.resources.folders { + v.id = maybe_gen_id(v.id.as_str(), ModelType::Folder, &mut id_map); + v.workspace_id = maybe_gen_id(v.workspace_id.as_str(), ModelType::Workspace, &mut id_map); + v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::Folder, &mut id_map); let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?; imported_resources.folders.push(x.clone()); info!("Imported folder: {}", x.name); } - for v in r.resources.http_requests { + for mut v in r.resources.http_requests { + v.id = maybe_gen_id(v.id.as_str(), ModelType::HttpRequest, &mut id_map); + v.workspace_id = maybe_gen_id(v.workspace_id.as_str(), ModelType::Workspace, &mut id_map); + v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::Folder, &mut id_map); let x = upsert_http_request(&w, v) .await .map_err(|e| e.to_string())?; @@ -785,7 +802,10 @@ async fn cmd_import_data( info!("Imported request: {}", x.name); } - for v in r.resources.grpc_requests { + for mut v in r.resources.grpc_requests { + v.id = maybe_gen_id(v.id.as_str(), ModelType::GrpcRequest, &mut id_map); + v.workspace_id = maybe_gen_id(v.workspace_id.as_str(), ModelType::Workspace, &mut id_map); + v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::Folder, &mut id_map); let x = upsert_grpc_request(&w, &v) .await .map_err(|e| e.to_string())?; @@ -1657,7 +1677,7 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { if !w.is_focused().unwrap() { return; } - + match event.id().0.as_str() { "quit" => exit(0), "close" => w.close().unwrap(), diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index de82ca40..2ed8f219 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -4,12 +4,41 @@ use std::fs; use log::error; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Sqlite}; -use sqlx::types::{Json, JsonValue}; use sqlx::types::chrono::NaiveDateTime; +use sqlx::types::{Json, JsonValue}; +use sqlx::{Pool, Sqlite}; use tauri::{AppHandle, Manager, WebviewWindow, Wry}; use tokio::sync::Mutex; +pub enum ModelType { + CookieJar, + Environment, + Folder, + GrpcConnection, + GrpcEvent, + GrpcRequest, + HttpRequest, + HttpResponse, + Workspace, +} + +impl ModelType { + pub fn id_prefix(&self) -> String { + match self { + ModelType::CookieJar => "cj", + ModelType::Environment => "ev", + ModelType::Folder => "fl", + ModelType::GrpcConnection => "gc", + ModelType::GrpcEvent => "ge", + ModelType::GrpcRequest => "gr", + ModelType::HttpRequest => "rq", + ModelType::HttpResponse => "rs", + ModelType::Workspace => "wk", + } + .to_string() + } +} + fn default_true() -> bool { true } @@ -481,10 +510,7 @@ pub async fn list_cookie_jars( .await } -pub async fn delete_cookie_jar( - window: &WebviewWindow, - id: &str, -) -> Result { +pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result { let cookie_jar = get_cookie_jar(window, id).await?; let db = get_db(window).await; @@ -516,7 +542,7 @@ pub async fn upsert_grpc_request( ) -> Result { let db = get_db(window).await; let id = match request.id.as_str() { - "" => generate_id(Some("gr")), + "" => generate_model_id(ModelType::GrpcRequest), _ => request.id.to_string(), }; let trimmed_name = request.name.trim(); @@ -612,7 +638,7 @@ pub async fn upsert_grpc_connection( ) -> Result { let db = get_db(window).await; let id = match connection.id.as_str() { - "" => generate_id(Some("gc")), + "" => generate_model_id(ModelType::GrpcConnection), _ => connection.id.to_string(), }; sqlx::query!( @@ -701,7 +727,7 @@ pub async fn upsert_grpc_event( ) -> Result { let db = get_db(window).await; let id = match event.id.as_str() { - "" => generate_id(Some("ge")), + "" => generate_model_id(ModelType::GrpcEvent), _ => event.id.to_string(), }; sqlx::query!( @@ -782,7 +808,7 @@ pub async fn upsert_cookie_jar( cookie_jar: &CookieJar, ) -> Result { let id = match cookie_jar.id.as_str() { - "" => generate_id(Some("cj")), + "" => generate_model_id(ModelType::CookieJar), _ => cookie_jar.id.to_string(), }; let trimmed_name = cookie_jar.name.trim(); @@ -914,7 +940,7 @@ pub async fn upsert_environment( environment: Environment, ) -> Result { let id = match environment.id.as_str() { - "" => generate_id(Some("ev")), + "" => generate_model_id(ModelType::Environment), _ => environment.id.to_string(), }; let trimmed_name = environment.name.trim(); @@ -1017,7 +1043,7 @@ pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result Result { let id = match r.id.as_str() { - "" => generate_id(Some("fl")), + "" => generate_model_id(ModelType::Folder), _ => r.id.to_string(), }; let trimmed_name = r.name.trim(); @@ -1064,7 +1090,7 @@ pub async fn upsert_http_request( r: HttpRequest, ) -> Result { let id = match r.id.as_str() { - "" => generate_id(Some("rq")), + "" => generate_model_id(ModelType::HttpRequest), _ => r.id.to_string(), }; let trimmed_name = r.name.trim(); @@ -1203,7 +1229,7 @@ pub async fn create_http_response( remote_addr: Option<&str>, ) -> Result { let req = get_http_request(window, request_id).await?; - let id = generate_id(Some("rp")); + let id = generate_model_id(ModelType::HttpResponse); let headers_json = Json(headers); let db = get_db(window).await; sqlx::query!( @@ -1281,7 +1307,7 @@ pub async fn upsert_workspace( workspace: Workspace, ) -> Result { let id = match workspace.id.as_str() { - "" => generate_id(Some("wk")), + "" => generate_model_id(ModelType::Workspace), _ => workspace.id.to_string(), }; let trimmed_name = workspace.name.trim(); @@ -1513,12 +1539,13 @@ pub async fn delete_all_http_responses( Ok(()) } -pub fn generate_id(prefix: Option<&str>) -> String { - let id = Alphanumeric.sample_string(&mut rand::thread_rng(), 10); - match prefix { - None => id, - Some(p) => format!("{p}_{id}"), - } +pub fn generate_model_id(model: ModelType) -> String { + let id = generate_id(); + format!("{}_{}", model.id_prefix(), id) +} + +pub fn generate_id() -> String { + Alphanumeric.sample_string(&mut rand::thread_rng(), 10) } #[derive(Default, Debug, Deserialize, Serialize)] @@ -1597,7 +1624,7 @@ struct ModelPayload { } fn emit_upserted_model(window: &WebviewWindow, model: M) -> M { - let payload = ModelPayload{ + let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), }; @@ -1607,7 +1634,7 @@ fn emit_upserted_model(window: &WebviewWindow, model: M) - } fn emit_deleted_model(window: &WebviewWindow, model: M) -> Result { - let payload = ModelPayload{ + let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), }; diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 7775444c..3b11805a 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -1,11 +1,11 @@ use std::fs; +use std::rc::Rc; use boa_engine::{ Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader, property::Attribute, Source, }; use boa_engine::builtins::promise::PromiseState; -use boa_engine::module::ModuleLoader; use boa_runtime::Console; use log::{debug, error}; use serde::{Deserialize, Serialize}; @@ -13,7 +13,7 @@ use serde_json::json; use tauri::{AppHandle, Manager}; use tauri::path::BaseDirectory; -use crate::models::{WorkspaceExportResources}; +use crate::models::WorkspaceExportResources; #[derive(Default, Debug, Deserialize, Serialize)] pub struct FilterResult { @@ -67,8 +67,7 @@ pub async fn run_plugin_import( return Ok(None); } - let resources: ImportResult = - serde_json::from_value(result_json).map_err(|e| e.to_string())?; + let resources: ImportResult = serde_json::from_value(result_json).map_err(|e| e.to_string())?; Ok(Some(resources)) } @@ -90,12 +89,9 @@ fn run_plugin( plugin_dir, plugin_index_file ); - // Module loader for the specific plugin - let loader = &SimpleModuleLoader::new(plugin_dir).expect("failed to create module loader"); - let dyn_loader: &dyn ModuleLoader = loader; - + let loader = Rc::new(SimpleModuleLoader::new(plugin_dir).unwrap()); let context = &mut Context::builder() - .module_loader(dyn_loader) + .module_loader(loader.clone()) .build() .expect("failed to create context"); @@ -109,15 +105,13 @@ fn run_plugin( // Insert parsed entrypoint into the module loader loader.insert(plugin_index_file, module.clone()); - let promise_result = module - .load_link_evaluate(context) - .expect("failed to evaluate module"); + let promise_result = module.load_link_evaluate(context); // Very important to push forward the job queue after queueing promises. context.run_jobs(); // Checking if the final promise didn't return an error. - match promise_result.state().expect("failed to get promise state") { + match promise_result.state() { PromiseState::Pending => { panic!("Promise was pending"); } diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx index f8131da9..84d5d008 100644 --- a/src-web/hooks/useImportData.tsx +++ b/src-web/hooks/useImportData.tsx @@ -18,7 +18,7 @@ export function useImportData() { const importData = async () => { const selected = await open({ - filters: [{ name: 'Export File', extensions: ['json', 'yaml'] }], + filters: [{ name: 'Export File', extensions: ['json', 'yaml', 'sh', 'txt'] }], multiple: false, }); if (selected == null) {