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

321 lines
8.4 KiB
TypeScript

import {
Collection as PMCollection,
FormParam,
Item,
ItemGroup,
QueryParam,
RequestAuthDefinition,
VariableDefinition,
} from "postman-collection"
import {
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
makeRESTRequest,
HoppCollection,
makeCollection,
ValidContentTypes,
knownContentTypes,
} from "@hoppscotch/data"
import { pipe, flow } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as A from "fp-ts/Array"
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 { PMRawLanguage } from "~/types/pm-coll-exts"
const safeParseJSON = (jsonStr: string) => O.tryCatch(() => JSON.parse(jsonStr))
const isPMItem = (x: unknown): x is Item => Item.isItem(x)
const replacePMVarTemplating = flow(
S.replace(/{{\s*/g, "<<"),
S.replace(/\s*}}/g, ">>")
)
const PMRawLanguageOptionsToContentTypeMap: Record<
PMRawLanguage,
ValidContentTypes
> = {
text: "text/plain",
javascript: "text/plain",
json: "application/json",
html: "text/html",
xml: "application/xml",
}
const isPMItemGroup = (x: unknown): x is ItemGroup<Item> =>
ItemGroup.isItemGroup(x)
const readPMCollection = (def: string) =>
pipe(
def,
safeParseJSON,
O.chain((data) => O.tryCatch(() => new PMCollection(data)))
)
const getHoppReqHeaders = (item: Item): HoppRESTHeader[] =>
pipe(
item.request.headers.all(),
A.map((header) => {
return <HoppRESTHeader>{
key: replacePMVarTemplating(header.key),
value: replacePMVarTemplating(header.value),
active: !header.disabled,
}
})
)
const getHoppReqParams = (item: Item): HoppRESTParam[] => {
return pipe(
item.request.url.query.all(),
A.filter(
(param): param is QueryParam & { key: string } =>
param.key !== undefined && param.key !== null && param.key.length > 0
),
A.map((param) => {
return <HoppRESTHeader>{
key: replacePMVarTemplating(param.key),
value: replacePMVarTemplating(param.value ?? ""),
active: !param.disabled,
}
})
)
}
type PMRequestAuthDef<
AuthType extends RequestAuthDefinition["type"] = RequestAuthDefinition["type"]
> = AuthType extends RequestAuthDefinition["type"] & string
? // eslint-disable-next-line no-unused-vars
{ type: AuthType } & { [x in AuthType]: VariableDefinition[] }
: { type: AuthType }
const getVariableValue = (defs: VariableDefinition[], key: string) =>
defs.find((param) => param.key === key)?.value as string | undefined
const getHoppReqAuth = (item: Item): HoppRESTAuth => {
if (!item.request.auth) return { authType: "none", authActive: true }
// Cast to the type for more stricter checking down the line
const auth = item.request.auth as unknown as PMRequestAuthDef
if (auth.type === "basic") {
return {
authType: "basic",
authActive: true,
username: replacePMVarTemplating(
getVariableValue(auth.basic, "username") ?? ""
),
password: replacePMVarTemplating(
getVariableValue(auth.basic, "password") ?? ""
),
}
} else if (auth.type === "apikey") {
return {
authType: "api-key",
authActive: true,
key: replacePMVarTemplating(getVariableValue(auth.apikey, "key") ?? ""),
value: replacePMVarTemplating(
getVariableValue(auth.apikey, "value") ?? ""
),
addTo:
(getVariableValue(auth.apikey, "in") ?? "query") === "query"
? "Query params"
: "Headers",
}
} else if (auth.type === "bearer") {
return {
authType: "bearer",
authActive: true,
token: replacePMVarTemplating(
getVariableValue(auth.bearer, "token") ?? ""
),
}
} else if (auth.type === "oauth2") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: replacePMVarTemplating(
getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
),
authURL: replacePMVarTemplating(
getVariableValue(auth.oauth2, "authUrl") ?? ""
),
clientID: replacePMVarTemplating(
getVariableValue(auth.oauth2, "clientId") ?? ""
),
scope: replacePMVarTemplating(
getVariableValue(auth.oauth2, "scope") ?? ""
),
token: replacePMVarTemplating(
getVariableValue(auth.oauth2, "accessToken") ?? ""
),
oidcDiscoveryURL: "",
}
}
return { authType: "none", authActive: true }
}
type PMFormDataParamType = FormParam & {
type: "file" | "text"
}
const getHoppReqBody = (item: Item): HoppRESTReqBody => {
if (!item.request.body) return { contentType: null, body: null }
// TODO: Implement
const body = item.request.body
if (body.mode === "formdata") {
return {
contentType: "multipart/form-data",
body:
(body.formdata?.all() as PMFormDataParamType[]).map((param) => ({
key: replacePMVarTemplating(param.key),
value: replacePMVarTemplating(
param.type === "text" ? (param.value as string) : ""
),
active: !param.disabled,
isFile: false, // TODO: Preserve isFile state ?
})) ?? [],
}
} else if (body.mode === "urlencoded") {
return {
contentType: "application/x-www-form-urlencoded",
body:
body.urlencoded
?.all()
.map(
(param) =>
`${replacePMVarTemplating(
param.key ?? ""
)}: ${replacePMVarTemplating(param.value ?? "")}`
)
.join("\n") ?? "",
}
} else if (body.mode === "raw") {
return pipe(
O.Do,
// Extract content-type
O.bind("contentType", () =>
pipe(
// Get the info from the content-type header
getHoppReqHeaders(item),
A.findFirst(({ key }) => key.toLowerCase() === "content-type"),
O.map((x) => x.value),
// Make sure it is a content-type Hopp can work with
O.filter(
(contentType): contentType is ValidContentTypes =>
contentType in knownContentTypes
),
// Back-up plan, assume language from raw language defintion
// If that too failed, just assume "text/plain"
O.getOrElse(() =>
pipe(
body.options?.raw?.language,
O.fromNullable,
O.map((lang) => PMRawLanguageOptionsToContentTypeMap[lang]),
O.getOrElse((): ValidContentTypes => "text/plain")
)
),
O.of
)
),
// Extract and parse body
O.bind("body", () =>
pipe(body.raw, O.fromNullable, O.map(replacePMVarTemplating))
),
// Return null content-type if failed, else return parsed
O.match(
() =>
<HoppRESTReqBody>{
contentType: null,
body: null,
},
({ contentType, body }) =>
<HoppRESTReqBody>{
contentType,
body,
}
)
)
}
// TODO: File
// TODO: GraphQL ?
return { contentType: null, body: null }
}
const getHoppReqURL = (item: Item): string =>
pipe(
item.request.url.toString(true),
S.replace(/\?.+/g, ""),
replacePMVarTemplating
)
const getHoppRequest = (item: Item): HoppRESTRequest => {
return makeRESTRequest({
name: item.name,
endpoint: getHoppReqURL(item),
method: item.request.method.toUpperCase(),
headers: getHoppReqHeaders(item),
params: getHoppReqParams(item),
auth: getHoppReqAuth(item),
body: getHoppReqBody(item),
// TODO: Decide about this
preRequestScript: "",
testScript: "",
})
}
const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: ig.name,
folders: pipe(
ig.items.all(),
A.filter(isPMItemGroup),
A.map(getHoppFolder)
),
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
})
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
export default defineImporter({
name: "import.from_postman",
icon: "postman",
steps: [
step({
stepName: "FILE_IMPORT",
metadata: {
caption: "import.from_postman_description",
acceptedFileTypes: ".json",
},
}),
] as const,
importer: ([fileContent]) =>
pipe(
// Try reading
fileContent,
readPMCollection,
O.map(flow(getHoppCollection, A.of)),
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
),
})