feat: migrate to vue 3 + vite (#2553)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com> Co-authored-by: liyasthomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppExporter } from "."
|
||||
|
||||
const exporter: HoppExporter<HoppCollection<HoppRESTRequest>> = (content) =>
|
||||
pipe(content, JSON.stringify, TE.right)
|
||||
|
||||
export default exporter
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
|
||||
export type HoppExporter<T> = (content: T) => TE.TaskEither<string, string>
|
||||
|
||||
export type HoppExporterDefinition<T> = {
|
||||
name: string
|
||||
exporter: () => Promise<HoppExporter<T>>
|
||||
}
|
||||
|
||||
export const RESTCollectionExporters: HoppExporterDefinition<
|
||||
HoppCollection<HoppRESTRequest>
|
||||
>[] = [
|
||||
{
|
||||
name: "Hoppscotch REST Collection JSON",
|
||||
exporter: () => import("./hopp").then((m) => m.default),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import * as O from "fp-ts/Option"
|
||||
import axios from "axios"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
// TODO: Add validation to output
|
||||
const fetchGist = (
|
||||
url: string
|
||||
): TO.TaskOption<HoppCollection<HoppRESTRequest>[]> =>
|
||||
pipe(
|
||||
TO.tryCatch(() =>
|
||||
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
})
|
||||
),
|
||||
TO.chain((res) =>
|
||||
pipe(
|
||||
O.tryCatch(() =>
|
||||
JSON.parse((Object.values(res.data.files)[0] as any).content)
|
||||
),
|
||||
TO.fromOption
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
id: "gist",
|
||||
name: "import.from_gist",
|
||||
icon: IconGithub,
|
||||
applicableTo: ["my-collections", "team-collections"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "URL_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_gist_description",
|
||||
placeholder: "import.gist_url",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) =>
|
||||
pipe(
|
||||
fetchGist(content),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import {
|
||||
translateToNewRESTCollection,
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { isPlainObject as _isPlainObject } from "lodash-es"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
export default defineImporter({
|
||||
id: "hoppscotch",
|
||||
name: "import.from_json",
|
||||
icon: IconFolderPlus,
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_json_description",
|
||||
acceptedFileTypes: "application/json",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) =>
|
||||
pipe(
|
||||
safeParseJSON(content),
|
||||
O.chain(
|
||||
flow(
|
||||
makeCollectionsArray,
|
||||
RA.map(
|
||||
flow(
|
||||
O.fromPredicate(isValidCollection),
|
||||
O.map(translateToNewRESTCollection)
|
||||
)
|
||||
),
|
||||
O.sequenceArray,
|
||||
O.map(RA.toArray)
|
||||
)
|
||||
),
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* checks if a value is a plain object
|
||||
*/
|
||||
const isPlainObject = (value: any): value is object => _isPlainObject(value)
|
||||
|
||||
/**
|
||||
* checks if a collection matches the schema for a hoppscotch collection.
|
||||
* as of now we are only checking if the collection has a "v" key in it.
|
||||
*/
|
||||
const isValidCollection = (
|
||||
collection: unknown
|
||||
): collection is HoppCollection<HoppRESTRequest> =>
|
||||
isPlainObject(collection) && "v" in collection
|
||||
|
||||
/**
|
||||
* convert single collection object into an array so it can be handled the same as multiple collections
|
||||
*/
|
||||
const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] =>
|
||||
Array.isArray(collections) ? collections : [collections]
|
||||
@@ -0,0 +1,22 @@
|
||||
import HoppRESTCollImporter from "./hopp"
|
||||
import OpenAPIImporter from "./openapi"
|
||||
import PostmanImporter from "./postman"
|
||||
import InsomniaImporter from "./insomnia"
|
||||
import GistImporter from "./gist"
|
||||
import MyCollectionsImporter from "./myCollections"
|
||||
|
||||
export const RESTCollectionImporters = [
|
||||
HoppRESTCollImporter,
|
||||
OpenAPIImporter,
|
||||
PostmanImporter,
|
||||
InsomniaImporter,
|
||||
GistImporter,
|
||||
MyCollectionsImporter,
|
||||
] as const
|
||||
|
||||
export const URLImporters = [
|
||||
HoppRESTCollImporter,
|
||||
OpenAPIImporter,
|
||||
PostmanImporter,
|
||||
InsomniaImporter,
|
||||
] as const
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import type { Component } from "vue"
|
||||
import { StepsOutputList } from "../steps"
|
||||
|
||||
/**
|
||||
* A common error state to be used when the file formats are not expected
|
||||
*/
|
||||
export const IMPORTER_INVALID_FILE_FORMAT =
|
||||
"importer_invalid_file_format" as const
|
||||
|
||||
export type HoppImporterError = typeof IMPORTER_INVALID_FILE_FORMAT
|
||||
|
||||
type HoppImporter<T, StepsType, Errors> = (
|
||||
stepValues: StepsOutputList<StepsType>
|
||||
) => TE.TaskEither<Errors, T>
|
||||
|
||||
type HoppImporterApplicableTo = Array<
|
||||
"team-collections" | "my-collections" | "url-import"
|
||||
>
|
||||
|
||||
/**
|
||||
* Definition for importers
|
||||
*/
|
||||
type HoppImporterDefinition<T, Y, E> = {
|
||||
/**
|
||||
* the id
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* Name of the importer, shown on the Select Importer dropdown
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Icon for importer button
|
||||
*/
|
||||
icon: Component
|
||||
|
||||
/**
|
||||
* Identifier for the importer
|
||||
*/
|
||||
applicableTo: HoppImporterApplicableTo
|
||||
|
||||
/**
|
||||
* The importer function, It is a Promise because its supposed to be loaded in lazily (dynamic imports ?)
|
||||
*/
|
||||
importer: HoppImporter<T, Y, E>
|
||||
|
||||
/**
|
||||
* The steps to fetch information required to run an importer
|
||||
*/
|
||||
steps: Y
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a Hoppscotch importer
|
||||
*/
|
||||
export const defineImporter = <ReturnType, StepType, Errors>(input: {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
importer: HoppImporter<ReturnType, StepType, Errors>
|
||||
applicableTo: HoppImporterApplicableTo
|
||||
steps: StepType
|
||||
}) => {
|
||||
return <HoppImporterDefinition<ReturnType, StepType, Errors>>{
|
||||
...input,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
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({
|
||||
id: "insomnia",
|
||||
name: "import.from_insomnia",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconInsomnia,
|
||||
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)
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter } from "."
|
||||
import { getRESTCollection } from "~/newstore/collections"
|
||||
|
||||
export default defineImporter({
|
||||
id: "myCollections",
|
||||
name: "import.from_my_collections",
|
||||
icon: IconUser,
|
||||
applicableTo: ["team-collections"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "TARGET_MY_COLLECTION",
|
||||
metadata: {
|
||||
caption: "import.from_my_collections_description",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) => pipe(content, getRESTCollection, A.of, TE.of),
|
||||
})
|
||||
@@ -0,0 +1,629 @@
|
||||
import IconOpenAPI from "~icons/lucide/file"
|
||||
import {
|
||||
OpenAPI,
|
||||
OpenAPIV2,
|
||||
OpenAPIV3,
|
||||
OpenAPIV3_1 as OpenAPIV31,
|
||||
} from "openapi-types"
|
||||
import SwaggerParser from "@apidevtools/swagger-parser"
|
||||
import yaml from "js-yaml"
|
||||
import {
|
||||
FormDataKeyValue,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
knownContentTypes,
|
||||
makeRESTRequest,
|
||||
HoppCollection,
|
||||
makeCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||
|
||||
// TODO: URL Import Support
|
||||
|
||||
const safeParseJSON = (str: string) => O.tryCatch(() => JSON.parse(str))
|
||||
|
||||
const safeParseYAML = (str: string) => O.tryCatch(() => yaml.load(str))
|
||||
|
||||
const objectHasProperty = <T extends string>(
|
||||
obj: unknown,
|
||||
propName: T
|
||||
// eslint-disable-next-line
|
||||
): obj is { [propName in T]: unknown } =>
|
||||
!!obj &&
|
||||
typeof obj === "object" &&
|
||||
Object.prototype.hasOwnProperty.call(obj, propName)
|
||||
|
||||
type OpenAPIPathInfoType =
|
||||
| OpenAPIV2.PathItemObject<Record<string, unknown>>
|
||||
| OpenAPIV3.PathItemObject<Record<string, unknown>>
|
||||
| OpenAPIV31.PathItemObject<Record<string, unknown>>
|
||||
|
||||
type OpenAPIParamsType =
|
||||
| OpenAPIV2.ParameterObject
|
||||
| OpenAPIV3.ParameterObject
|
||||
| OpenAPIV31.ParameterObject
|
||||
|
||||
type OpenAPIOperationType =
|
||||
| OpenAPIV2.OperationObject
|
||||
| OpenAPIV3.OperationObject
|
||||
| OpenAPIV31.OperationObject
|
||||
|
||||
// Removes the OpenAPI Path Templating to the Hoppscotch Templating (<< ? >>)
|
||||
const replaceOpenApiPathTemplating = flow(
|
||||
S.replace(/{/g, "<<"),
|
||||
S.replace(/}/g, ">>")
|
||||
)
|
||||
|
||||
const parseOpenAPIParams = (params: OpenAPIParamsType[]): HoppRESTParam[] =>
|
||||
pipe(
|
||||
params,
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "query"),
|
||||
O.map(
|
||||
(param) =>
|
||||
<HoppRESTParam>{
|
||||
key: param.name,
|
||||
value: "", // TODO: Can we do anything more ? (parse default values maybe)
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
|
||||
pipe(
|
||||
params,
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "header"),
|
||||
O.map(
|
||||
(header) =>
|
||||
<HoppRESTParam>{
|
||||
key: header.name,
|
||||
value: "", // TODO: Can we do anything more ? (parse default values maybe)
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => {
|
||||
const obj = (op.consumes ?? [])[0] as string | undefined
|
||||
|
||||
// Not a content-type Hoppscotch supports
|
||||
if (!obj || !(obj in knownContentTypes))
|
||||
return { contentType: null, body: null }
|
||||
|
||||
// Textual Content Types, so we just parse it and keep
|
||||
if (
|
||||
obj !== "multipart/form-data" &&
|
||||
obj !== "application/x-www-form-urlencoded"
|
||||
)
|
||||
return { contentType: obj as any, body: "" }
|
||||
|
||||
const formDataValues = pipe(
|
||||
(op.parameters ?? []) as OpenAPIV2.Parameter[],
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "body"),
|
||||
O.map(
|
||||
(param) =>
|
||||
<FormDataKeyValue>{
|
||||
key: param.name,
|
||||
isFile: false,
|
||||
value: "",
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return obj === "application/x-www-form-urlencoded"
|
||||
? {
|
||||
contentType: obj,
|
||||
body: formDataValues.map(({ key }) => `${key}: `).join("\n"),
|
||||
}
|
||||
: { contentType: obj, body: formDataValues }
|
||||
}
|
||||
|
||||
const parseOpenAPIV3BodyFormData = (
|
||||
contentType: "multipart/form-data" | "application/x-www-form-urlencoded",
|
||||
mediaObj: OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
|
||||
): HoppRESTReqBody => {
|
||||
const schema = mediaObj.schema as
|
||||
| OpenAPIV3.SchemaObject
|
||||
| OpenAPIV31.SchemaObject
|
||||
| undefined
|
||||
|
||||
if (!schema || schema.type !== "object") {
|
||||
return contentType === "application/x-www-form-urlencoded"
|
||||
? { contentType, body: "" }
|
||||
: { contentType, body: [] }
|
||||
}
|
||||
|
||||
const keys = Object.keys(schema.properties ?? {})
|
||||
|
||||
if (contentType === "application/x-www-form-urlencoded") {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map((key) => `${key}: `).join("\n"),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) =>
|
||||
<FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseOpenAPIV3Body = (
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
|
||||
): HoppRESTReqBody => {
|
||||
const objs = Object.entries(
|
||||
(
|
||||
op.requestBody as
|
||||
| OpenAPIV3.RequestBodyObject
|
||||
| OpenAPIV31.RequestBodyObject
|
||||
| undefined
|
||||
)?.content ?? {}
|
||||
)
|
||||
|
||||
if (objs.length === 0) return { contentType: null, body: null }
|
||||
|
||||
// We only take the first definition
|
||||
const [contentType, media]: [
|
||||
string,
|
||||
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
|
||||
] = objs[0]
|
||||
|
||||
return contentType in knownContentTypes
|
||||
? contentType === "multipart/form-data" ||
|
||||
contentType === "application/x-www-form-urlencoded"
|
||||
? parseOpenAPIV3BodyFormData(contentType, media)
|
||||
: { contentType: contentType as any, body: "" }
|
||||
: { contentType: null, body: null }
|
||||
}
|
||||
|
||||
const isOpenAPIV3Operation = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): op is OpenAPIV3.OperationObject | OpenAPIV31.OperationObject =>
|
||||
objectHasProperty(doc, "openapi") &&
|
||||
typeof doc.openapi === "string" &&
|
||||
doc.openapi.startsWith("3.")
|
||||
|
||||
const parseOpenAPIBody = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): HoppRESTReqBody =>
|
||||
isOpenAPIV3Operation(doc, op)
|
||||
? parseOpenAPIV3Body(op)
|
||||
: parseOpenAPIV2Body(op)
|
||||
|
||||
const resolveOpenAPIV3SecurityObj = (
|
||||
scheme: OpenAPIV3.SecuritySchemeObject | OpenAPIV31.SecuritySchemeObject,
|
||||
_schemeData: string[] // Used for OAuth to pass params
|
||||
): HoppRESTAuth => {
|
||||
if (scheme.type === "http") {
|
||||
if (scheme.scheme === "basic") {
|
||||
// Basic
|
||||
return { authType: "basic", authActive: true, username: "", password: "" }
|
||||
} else if (scheme.scheme === "bearer") {
|
||||
// Bearer
|
||||
return { authType: "bearer", authActive: true, token: "" }
|
||||
} else {
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
} else if (scheme.type === "apiKey") {
|
||||
if (scheme.in === "header") {
|
||||
return {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
addTo: "Headers",
|
||||
key: scheme.name,
|
||||
value: "",
|
||||
}
|
||||
} else if (scheme.in === "query") {
|
||||
return {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
addTo: "Query params",
|
||||
key: scheme.in,
|
||||
value: "",
|
||||
}
|
||||
}
|
||||
} else if (scheme.type === "oauth2") {
|
||||
// NOTE: We select flow on a first come basis on this order, authorizationCode > implicit > password > clientCredentials
|
||||
if (scheme.flows.authorizationCode) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.authorizationCode.tokenUrl ?? "",
|
||||
authURL: scheme.flows.authorizationCode.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.implicit) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: scheme.flows.implicit.authorizationUrl ?? "",
|
||||
accessTokenURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.password) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: "",
|
||||
accessTokenURL: scheme.flows.password.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.clientCredentials) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.clientCredentials.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
} else if (scheme.type === "openIdConnect") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: scheme.openIdConnectUrl ?? "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3SecurityScheme = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
schemeName: string,
|
||||
schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
const scheme = doc.components?.securitySchemes?.[schemeName] as
|
||||
| OpenAPIV3.SecuritySchemeObject
|
||||
| undefined
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
else return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3Security = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
security:
|
||||
| OpenAPIV3.SecurityRequirementObject[]
|
||||
| OpenAPIV31.SecurityRequirementObject[]
|
||||
): HoppRESTAuth => {
|
||||
// NOTE: Hoppscotch only considers the first security requirement
|
||||
const sec = security[0] as OpenAPIV3.SecurityRequirementObject | undefined
|
||||
|
||||
if (!sec) return { authType: "none", authActive: true }
|
||||
|
||||
// NOTE: We only consider the first security condition within the first condition
|
||||
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
|
||||
undefined,
|
||||
undefined,
|
||||
]) as [string | undefined, string[] | undefined]
|
||||
|
||||
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
|
||||
|
||||
return resolveOpenAPIV3SecurityScheme(doc, schemeName, schemeData)
|
||||
}
|
||||
|
||||
const parseOpenAPIV3Auth = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
|
||||
): HoppRESTAuth => {
|
||||
const rootAuth = doc.security
|
||||
? resolveOpenAPIV3Security(doc, doc.security)
|
||||
: undefined
|
||||
const opAuth = op.security
|
||||
? resolveOpenAPIV3Security(doc, op.security)
|
||||
: undefined
|
||||
|
||||
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2SecurityScheme = (
|
||||
scheme: OpenAPIV2.SecuritySchemeObject,
|
||||
_schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
if (scheme.type === "basic") {
|
||||
return { authType: "basic", authActive: true, username: "", password: "" }
|
||||
} else if (scheme.type === "apiKey") {
|
||||
// V2 only supports in: header and in: query
|
||||
return {
|
||||
authType: "api-key",
|
||||
addTo: scheme.in === "header" ? "Headers" : "Query params",
|
||||
authActive: true,
|
||||
key: scheme.name,
|
||||
value: "",
|
||||
}
|
||||
} else if (scheme.type === "oauth2") {
|
||||
// NOTE: We select flow on a first come basis on this order, accessCode > implicit > password > application
|
||||
if (scheme.flow === "accessCode") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "implicit") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "application") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "password") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2SecurityDef = (
|
||||
doc: OpenAPIV2.Document,
|
||||
schemeName: string,
|
||||
schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
const scheme = Object.entries(doc.securityDefinitions ?? {}).find(
|
||||
([name]) => schemeName === name
|
||||
)
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
|
||||
const schemeObj = scheme[1]
|
||||
|
||||
return resolveOpenAPIV2SecurityScheme(schemeObj, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2Security = (
|
||||
doc: OpenAPIV2.Document,
|
||||
security: OpenAPIV2.SecurityRequirementObject[]
|
||||
): HoppRESTAuth => {
|
||||
// NOTE: Hoppscotch only considers the first security requirement
|
||||
const sec = security[0] as OpenAPIV2.SecurityRequirementObject | undefined
|
||||
|
||||
if (!sec) return { authType: "none", authActive: true }
|
||||
|
||||
// NOTE: We only consider the first security condition within the first condition
|
||||
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
|
||||
undefined,
|
||||
undefined,
|
||||
]) as [string | undefined, string[] | undefined]
|
||||
|
||||
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
|
||||
|
||||
return resolveOpenAPIV2SecurityDef(doc, schemeName, schemeData)
|
||||
}
|
||||
|
||||
const parseOpenAPIV2Auth = (
|
||||
doc: OpenAPIV2.Document,
|
||||
op: OpenAPIV2.OperationObject
|
||||
): HoppRESTAuth => {
|
||||
const rootAuth = doc.security
|
||||
? resolveOpenAPIV2Security(doc, doc.security)
|
||||
: undefined
|
||||
const opAuth = op.security
|
||||
? resolveOpenAPIV2Security(doc, op.security)
|
||||
: undefined
|
||||
|
||||
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const parseOpenAPIAuth = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): HoppRESTAuth =>
|
||||
isOpenAPIV3Operation(doc, op)
|
||||
? parseOpenAPIV3Auth(doc as OpenAPIV3.Document | OpenAPIV31.Document, op)
|
||||
: parseOpenAPIV2Auth(doc as OpenAPIV2.Document, op)
|
||||
|
||||
const convertPathToHoppReqs = (
|
||||
doc: OpenAPI.Document,
|
||||
pathName: string,
|
||||
pathObj: OpenAPIPathInfoType
|
||||
) =>
|
||||
pipe(
|
||||
["get", "head", "post", "put", "delete", "options", "patch"] as const,
|
||||
|
||||
// Filter and map out path info
|
||||
RA.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((method) => !!pathObj[method]),
|
||||
O.map((method) => ({ method, info: pathObj[method]! }))
|
||||
)
|
||||
),
|
||||
|
||||
// Construct request object
|
||||
RA.map(({ method, info }) =>
|
||||
makeRESTRequest({
|
||||
name: info.operationId ?? info.summary ?? "Untitled Request",
|
||||
method: method.toUpperCase(),
|
||||
endpoint: `<<baseUrl>>${replaceOpenApiPathTemplating(pathName)}`, // TODO: Make this proper
|
||||
|
||||
// We don't need to worry about reference types as the Dereferencing pass should remove them
|
||||
params: parseOpenAPIParams(
|
||||
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
|
||||
),
|
||||
headers: parseOpenAPIHeaders(
|
||||
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
|
||||
),
|
||||
|
||||
auth: parseOpenAPIAuth(doc, info),
|
||||
|
||||
body: parseOpenAPIBody(doc, info),
|
||||
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
),
|
||||
|
||||
// Disable Readonly
|
||||
RA.toArray
|
||||
)
|
||||
|
||||
const convertOpenApiDocToHopp = (
|
||||
doc: OpenAPI.Document
|
||||
): TE.TaskEither<never, HoppCollection<HoppRESTRequest>[]> => {
|
||||
const name = doc.info.title
|
||||
|
||||
const paths = Object.entries(doc.paths ?? {})
|
||||
.map(([pathName, pathObj]) => convertPathToHoppReqs(doc, pathName, pathObj))
|
||||
.flat()
|
||||
|
||||
return TE.of([
|
||||
makeCollection<HoppRESTRequest>({
|
||||
name,
|
||||
folders: [],
|
||||
requests: paths,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const parseOpenAPIDocContent = (str: string) =>
|
||||
pipe(
|
||||
str,
|
||||
safeParseJSON,
|
||||
O.match(
|
||||
() => safeParseYAML(str),
|
||||
(data) => O.of(data)
|
||||
)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
id: "openapi",
|
||||
name: "import.from_openapi",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconOpenAPI,
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_openapi_description",
|
||||
acceptedFileTypes: ".json, .yaml, .yml",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([fileContent]) =>
|
||||
pipe(
|
||||
// See if we can parse JSON properly
|
||||
fileContent,
|
||||
parseOpenAPIDocContent,
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||
// Try validating, else the importer is invalid file format
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.validate(obj),
|
||||
() => IMPORTER_INVALID_FILE_FORMAT
|
||||
)
|
||||
)
|
||||
),
|
||||
// Deference the references
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.dereference(obj),
|
||||
() => OPENAPI_DEREF_ERROR
|
||||
)
|
||||
)
|
||||
),
|
||||
TE.chainW(convertOpenApiDocToHopp)
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import {
|
||||
Collection as PMCollection,
|
||||
Item,
|
||||
ItemGroup,
|
||||
QueryParam,
|
||||
RequestAuthDefinition,
|
||||
VariableDefinition,
|
||||
} from "postman-collection"
|
||||
import {
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
makeRESTRequest,
|
||||
HoppCollection,
|
||||
makeCollection,
|
||||
ValidContentTypes,
|
||||
knownContentTypes,
|
||||
FormDataKeyValue,
|
||||
} 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"
|
||||
import { stringArrayJoin } from "~/helpers/functional/array"
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
const getHoppReqBody = (item: Item): HoppRESTReqBody => {
|
||||
if (!item.request.body) return { contentType: null, body: null }
|
||||
|
||||
const body = item.request.body
|
||||
|
||||
if (body.mode === "formdata") {
|
||||
return {
|
||||
contentType: "multipart/form-data",
|
||||
body: pipe(
|
||||
body.formdata?.all() ?? [],
|
||||
A.map(
|
||||
(param) =>
|
||||
<FormDataKeyValue>{
|
||||
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: pipe(
|
||||
body.urlencoded?.all() ?? [],
|
||||
A.map(
|
||||
(param) =>
|
||||
`${replacePMVarTemplating(
|
||||
param.key ?? ""
|
||||
)}: ${replacePMVarTemplating(param.value ?? "")}`
|
||||
),
|
||||
stringArrayJoin("\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
|
||||
O.alt(() =>
|
||||
pipe(
|
||||
body.options?.raw?.language,
|
||||
O.fromNullable,
|
||||
O.map((lang) => PMRawLanguageOptionsToContentTypeMap[lang])
|
||||
)
|
||||
),
|
||||
|
||||
// If that too failed, just assume "text/plain"
|
||||
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(false),
|
||||
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({
|
||||
id: "postman",
|
||||
name: "import.from_postman",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconPostman,
|
||||
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)
|
||||
),
|
||||
})
|
||||
82
packages/hoppscotch-app/src/helpers/import-export/steps.ts
Normal file
82
packages/hoppscotch-app/src/helpers/import-export/steps.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Defines which type of content a Step returns.
|
||||
* Add an entry here when you define a step
|
||||
*/
|
||||
export type StepDefinition = {
|
||||
FILE_IMPORT: {
|
||||
returnType: string
|
||||
metadata: {
|
||||
caption: string
|
||||
acceptedFileTypes: string
|
||||
}
|
||||
} // String content of the file
|
||||
TARGET_MY_COLLECTION: {
|
||||
returnType: number
|
||||
metadata: {
|
||||
caption: string
|
||||
}
|
||||
} // folderPath
|
||||
URL_IMPORT: {
|
||||
returnType: string
|
||||
metadata: {
|
||||
caption: string
|
||||
placeholder: string
|
||||
}
|
||||
} // String content of the url
|
||||
}
|
||||
|
||||
export type StepReturnValue = StepDefinition[keyof StepDefinition]["returnType"]
|
||||
|
||||
/**
|
||||
* Defines what the data structure of a step
|
||||
*/
|
||||
export type Step<T extends keyof StepDefinition> =
|
||||
StepDefinition[T]["metadata"] extends never
|
||||
? {
|
||||
name: T
|
||||
caption?: string
|
||||
metadata: undefined
|
||||
}
|
||||
: {
|
||||
name: T
|
||||
caption?: string
|
||||
metadata: StepDefinition[T]["metadata"]
|
||||
}
|
||||
|
||||
/**
|
||||
* The return output value of an individual step
|
||||
*/
|
||||
export type StepReturnType<T> = T extends Step<infer U>
|
||||
? StepDefinition[U]["returnType"]
|
||||
: never
|
||||
|
||||
export type StepMetadata<T> = T extends Step<infer U>
|
||||
? StepDefinition[U]["metadata"]
|
||||
: never
|
||||
|
||||
/**
|
||||
* Defines the value of the output list generated by a step
|
||||
*/
|
||||
export type StepsOutputList<T> = {
|
||||
[K in keyof T]: StepReturnType<T[K]>
|
||||
}
|
||||
|
||||
type StepFuncInput<T extends keyof StepDefinition> =
|
||||
StepDefinition[T]["metadata"] extends never
|
||||
? {
|
||||
stepName: T
|
||||
caption?: string
|
||||
}
|
||||
: {
|
||||
stepName: T
|
||||
caption?: string
|
||||
metadata: StepDefinition[T]["metadata"]
|
||||
}
|
||||
|
||||
/** Use this function to define a step */
|
||||
export const step = <T extends keyof StepDefinition>(input: StepFuncInput<T>) =>
|
||||
<Step<T>>{
|
||||
name: input.stepName,
|
||||
metadata: (input as any).metadata ?? undefined,
|
||||
caption: input.caption,
|
||||
}
|
||||
Reference in New Issue
Block a user