From 05ca0c096612ead7ee1fc2f2f0b77ee7ef024969 Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Mon, 10 Jan 2022 21:16:36 +0530 Subject: [PATCH] refactor: port postman and insomnia importers --- .../helpers/import-export/import/importers.ts | 4 + .../helpers/import-export/import/insomnia.ts | 54 +++++ .../helpers/import-export/import/postman.ts | 203 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 packages/hoppscotch-app/helpers/import-export/import/insomnia.ts create mode 100644 packages/hoppscotch-app/helpers/import-export/import/postman.ts diff --git a/packages/hoppscotch-app/helpers/import-export/import/importers.ts b/packages/hoppscotch-app/helpers/import-export/import/importers.ts index 90bed3887..43c49e686 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/importers.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/importers.ts @@ -1,7 +1,11 @@ import HoppRESTCollImporter from "./hopp" import OpenAPIImporter from "./openapi" +import PostmanImporter from "./postman" +import InsomniaImporter from "./insomnia" export const RESTCollectionImporters = [ HoppRESTCollImporter, OpenAPIImporter, + PostmanImporter, + InsomniaImporter, ] as const diff --git a/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts b/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts new file mode 100644 index 000000000..3168c9693 --- /dev/null +++ b/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts @@ -0,0 +1,54 @@ +import * as TE from "fp-ts/TaskEither" +import { HoppRESTRequest } from "@hoppscotch/data" +import { step } from "../steps" +import { parsePostmanCollection } from "./postman" +import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "." +import { parseInsomniaCollection } from "~/helpers/utils/parseInsomniaCollection" +import { Collection } from "~/newstore/collections" + +// This Importer definition is less than ideal, +// would love an attempt at refactoring this + +export default defineImporter({ + name: "Insomnia Collection", + steps: [ + step({ + stepName: "FILE_OR_URL_IMPORT", + metadata: { + acceptedFileTypes: ".json", + }, + }), + ] as const, + importer: ([fileContent]) => { + try { + let collections = parseInsomniaCollection(fileContent) + const content = JSON.stringify(collections) + + if (collections[0]) { + const [name, folders, requests] = Object.keys(collections[0]) + if ( + name === "name" && + folders === "folders" && + requests === "requests" + ) { + return TE.right(collections as Collection[]) + } + + return TE.left(IMPORTER_INVALID_FILE_FORMAT) + } else if ( + collections.info && + collections.info.schema.includes("v2.1.0") + ) { + // replace the variables, postman uses {{var}}, Hoppscotch uses <> + collections = JSON.parse(content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")) + collections = [parsePostmanCollection(collections)] + + return TE.right(collections as Collection[]) + } else { + return TE.left(IMPORTER_INVALID_FILE_FORMAT) + } + } catch (_e) { + return TE.left(IMPORTER_INVALID_FILE_FORMAT) + } + }, +}) diff --git a/packages/hoppscotch-app/helpers/import-export/import/postman.ts b/packages/hoppscotch-app/helpers/import-export/import/postman.ts new file mode 100644 index 000000000..2ee0f0d8b --- /dev/null +++ b/packages/hoppscotch-app/helpers/import-export/import/postman.ts @@ -0,0 +1,203 @@ +import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data" +import { pipe } from "fp-ts/function" +import * as O from "fp-ts/Option" +import * as TE from "fp-ts/TaskEither" +import { step } from "../steps" +import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "." +import { Collection, makeCollection } from "~/newstore/collections" + +// TODO: I don't even know what is going on here :/ +type PostmanCollection = { + info?: { + name: string + } + name: string + item: { + name: string + request: any + item?: any + }[] + folders?: any +} + +const hasFolder = (item: { item?: any }) => { + return Object.prototype.hasOwnProperty.call(item, "item") +} + +export const parsePostmanCollection = ({ + info, + name, + item, +}: PostmanCollection) => { + const hoppscotchCollection: Collection = makeCollection({ + name: "", + folders: [], + requests: [], + }) + + hoppscotchCollection.name = info ? info.name : name + + if (item && item.length > 0) { + for (const collectionItem of item) { + if (collectionItem.request) { + if ( + Object.prototype.hasOwnProperty.call(hoppscotchCollection, "folders") + ) { + hoppscotchCollection.name = info ? info.name : name + hoppscotchCollection.requests.push( + parsePostmanRequest(collectionItem) + ) + } else { + hoppscotchCollection.name = name || "" + hoppscotchCollection.requests.push( + parsePostmanRequest(collectionItem) + ) + } + } else if (hasFolder(collectionItem)) { + hoppscotchCollection.folders.push( + parsePostmanCollection(collectionItem as any) + ) + } else { + hoppscotchCollection.requests.push(parsePostmanRequest(collectionItem)) + } + } + } + return hoppscotchCollection +} + +// TODO: Rewrite +const parsePostmanRequest = ({ + name, + request, +}: { + name: string + request: any +}) => { + const pwRequest = { + url: "", + path: "", + method: "", + auth: "", + httpUser: "", + httpPassword: "", + passwordFieldType: "password", + bearerToken: "", + headers: [] as { name?: string; type?: string }[], + params: [] as { disabled?: boolean }[], + bodyParams: [] as { type?: string }[], + body: { + body: "", + contentType: "application/json", + }, + rawParams: "", + rawInput: false, + contentType: "", + requestType: "", + name: "", + } + + pwRequest.name = name + if (request.url) { + const requestObjectUrl = request.url.raw.match( + /^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/ + ) + if (requestObjectUrl) { + pwRequest.url = requestObjectUrl[1] + pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : "" + } else { + pwRequest.url = request.url.raw + } + } + + pwRequest.method = request.method + const itemAuth = request.auth ? request.auth : "" + const authType = itemAuth ? itemAuth.type : "" + + try { + if (authType === "basic") { + pwRequest.auth = "Basic Auth" + pwRequest.httpUser = + itemAuth.basic[0].key === "username" + ? itemAuth.basic[0].value + : itemAuth.basic[1].value + pwRequest.httpPassword = + itemAuth.basic[0].key === "password" + ? itemAuth.basic[0].value + : itemAuth.basic[1].value + } else if (authType === "oauth2") { + pwRequest.auth = "OAuth 2.0" + pwRequest.bearerToken = + itemAuth.oauth2[0].key === "accessToken" + ? itemAuth.oauth2[0].value + : itemAuth.oauth2[1].value + } else if (authType === "bearer") { + pwRequest.auth = "Bearer Token" + pwRequest.bearerToken = itemAuth.bearer[0].value + } + } catch (error) { + console.error(error) + } + + const requestObjectHeaders = request.header + if (requestObjectHeaders) { + pwRequest.headers = requestObjectHeaders + for (const header of pwRequest.headers) { + delete header.name + delete header.type + } + } + if (request.url) { + const requestObjectParams = request.url.query + if (requestObjectParams) { + pwRequest.params = requestObjectParams + for (const param of pwRequest.params) { + delete param.disabled + } + } + } + if (request.body) { + if (request.body.mode === "urlencoded") { + const params = request.body.urlencoded + pwRequest.bodyParams = params || [] + for (const param of pwRequest.bodyParams) { + delete param.type + } + } else if (request.body.mode === "raw") { + pwRequest.rawInput = true + pwRequest.rawParams = request.body.raw + try { + const body = JSON.parse(request.body.raw) + pwRequest.body.body = JSON.stringify(body, null, 2) + } catch (error) { + console.error(error) + } + } + } + return translateToNewRequest(pwRequest) +} + +const safeParseJSON = (str: string) => O.tryCatch(() => JSON.parse(str)) + +export default defineImporter({ + name: "Postman Collection", + steps: [ + step({ + stepName: "FILE_OR_URL_IMPORT", + metadata: { + acceptedFileTypes: ".json", + }, + }), + ] as const, + importer: ([fileContent]) => + pipe( + // Parse to JSON + fileContent, + safeParseJSON, + + // Parse To Postman Collection + O.chain((data) => O.tryCatch(() => parsePostmanCollection(data))), + + // Convert Option to Task Either + TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT) + ), +})