Files
hoppscotch/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts

234 lines
6.5 KiB
TypeScript

import { convert, ImportRequest } from "insomnia-importers"
import { pipe, flow } from "fp-ts/function"
import {
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
knownContentTypes,
makeRESTRequest,
HoppCollection,
makeCollection,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as S from "fp-ts/string"
import * as TO from "fp-ts/TaskOption"
import * as TE from "fp-ts/TaskEither"
import { step } from "../steps"
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Y>
? Y
: never
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
type InsomniaResource = ImportRequest
type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
type InsomniaRequestResource = ImportRequest & { _type: "request" }
const parseInsomniaDoc = (content: string) =>
TO.tryCatch(() => convert(content))
const replaceVarTemplating = flow(
S.replace(/{{\s*/g, "<<"),
S.replace(/\s*}}/g, ">>")
)
const getFoldersIn = (
folder: InsomniaFolderResource | null,
resources: InsomniaResource[]
) =>
pipe(
resources,
A.filter(
(x): x is InsomniaFolderResource =>
(x._type === "request_group" || x._type === "workspace") &&
x.parentId === (folder?._id ?? null)
)
)
const getRequestsIn = (
folder: InsomniaFolderResource | null,
resources: InsomniaResource[]
) =>
pipe(
resources,
A.filter(
(x): x is InsomniaRequestResource =>
x._type === "request" && x.parentId === (folder?._id ?? null)
)
)
/**
* The provided type by insomnia-importers, this type corrects it
*/
type InsoReqAuth =
| { type: "basic"; disabled?: boolean; username?: string; password?: string }
| {
type: "oauth2"
disabled?: boolean
accessTokenUrl?: string
authorizationUrl?: string
clientId?: string
scope?: string
}
| {
type: "bearer"
disabled?: boolean
token?: string
}
const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
if (!req.authentication) return { authType: "none", authActive: true }
const auth = req.authentication as InsoReqAuth
if (auth.type === "basic")
return {
authType: "basic",
authActive: true,
username: replaceVarTemplating(auth.username ?? ""),
password: replaceVarTemplating(auth.password ?? ""),
}
else if (auth.type === "oauth2")
return {
authType: "oauth-2",
authActive: !(auth.disabled ?? false),
accessTokenURL: replaceVarTemplating(auth.accessTokenUrl ?? ""),
authURL: replaceVarTemplating(auth.authorizationUrl ?? ""),
clientID: replaceVarTemplating(auth.clientId ?? ""),
oidcDiscoveryURL: "",
scope: replaceVarTemplating(auth.scope ?? ""),
token: "",
}
else if (auth.type === "bearer")
return {
authType: "bearer",
authActive: true,
token: replaceVarTemplating(auth.token ?? ""),
}
return { authType: "none", authActive: true }
}
const getHoppReqBody = (req: InsomniaRequestResource): HoppRESTReqBody => {
if (!req.body) return { contentType: null, body: null }
if (typeof req.body === "string") {
const contentType =
req.headers?.find(
(header) => header.name.toLowerCase() === "content-type"
)?.value ?? "text/plain"
return { contentType, body: replaceVarTemplating(req.body) }
}
if (req.body.mimeType === "multipart/form-data") {
return {
contentType: "multipart/form-data",
body:
req.body.params?.map((param) => ({
key: replaceVarTemplating(param.name),
value: replaceVarTemplating(param.value ?? ""),
active: !(param.disabled ?? false),
isFile: false,
})) ?? [],
}
} else if (req.body.mimeType === "application/x-www-form-urlencoded") {
return {
contentType: "application/x-www-form-urlencoded",
body:
req.body.params
?.filter((param) => !(param.disabled ?? false))
.map(
(param) =>
`${replaceVarTemplating(param.name)}: ${replaceVarTemplating(
param.value ?? ""
)}`
)
.join("\n") ?? "",
}
} else if (
Object.keys(knownContentTypes).includes(req.body.mimeType ?? "text/plain")
) {
return {
contentType: (req.body.mimeType ?? "text/plain") as any,
body: replaceVarTemplating(req.body.text ?? "") as any,
}
}
return { contentType: null, body: null }
}
const getHoppReqHeaders = (req: InsomniaRequestResource): HoppRESTHeader[] =>
req.headers?.map((header) => ({
key: replaceVarTemplating(header.name),
value: replaceVarTemplating(header.value),
active: !header.disabled,
})) ?? []
const getHoppReqParams = (req: InsomniaRequestResource): HoppRESTParam[] =>
req.parameters?.map((param) => ({
key: replaceVarTemplating(param.name),
value: replaceVarTemplating(param.value ?? ""),
active: !(param.disabled ?? false),
})) ?? []
const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
makeRESTRequest({
name: req.name ?? "Untitled Request",
method: req.method?.toUpperCase() ?? "GET",
endpoint: replaceVarTemplating(req.url ?? ""),
auth: getHoppReqAuth(req),
body: getHoppReqBody(req),
headers: getHoppReqHeaders(req),
params: getHoppReqParams(req),
preRequestScript: "",
testScript: "",
})
const getHoppFolder = (
folderRes: InsomniaFolderResource,
resources: InsomniaResource[]
): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: folderRes.name ?? "",
folders: getFoldersIn(folderRes, resources).map((f) =>
getHoppFolder(f, resources)
),
requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
})
const getHoppCollections = (doc: InsomniaDoc) =>
getFoldersIn(null, doc.data.resources).map((f) =>
getHoppFolder(f, doc.data.resources)
)
export default defineImporter({
name: "import.from_insomnia",
icon: "insomnia",
steps: [
step({
stepName: "FILE_IMPORT",
metadata: {
caption: "import.from_insomnia_description",
acceptedFileTypes: ".json, .yaml",
},
}),
] as const,
importer: ([fileContent]) =>
pipe(
fileContent,
parseInsomniaDoc,
TO.map(getHoppCollections),
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
),
})