refactor: revamp the importers & exporters systems to be reused (#3425)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ export function translateExtURLParams(
|
||||
initialReq?: HoppRESTRequest
|
||||
): HoppRESTRequest {
|
||||
if (urlParams.v) return parseV1ExtURL(urlParams, initialReq)
|
||||
else return parseV0ExtURL(urlParams, initialReq)
|
||||
return parseV0ExtURL(urlParams, initialReq)
|
||||
}
|
||||
|
||||
function parseV0ExtURL(
|
||||
|
||||
@@ -325,7 +325,8 @@ export const runAuthOnlyGQLSubscription = flow(
|
||||
) {
|
||||
sub.unsubscribe()
|
||||
return null
|
||||
} else return res
|
||||
}
|
||||
return res
|
||||
}),
|
||||
filter((res): res is Exclude<typeof res, null> => res !== null)
|
||||
)
|
||||
|
||||
@@ -132,17 +132,10 @@ export const teamCollToHoppRESTColl = (
|
||||
* @param teamID - ID of the team
|
||||
* @returns Either of the JSON string of the collection or the error
|
||||
*/
|
||||
export const getTeamCollectionJSON = async (teamID: string) => {
|
||||
const data = await runGQLQuery({
|
||||
export const getTeamCollectionJSON = async (teamID: string) =>
|
||||
await runGQLQuery({
|
||||
query: ExportAsJsonDocument,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
return E.left(data.left)
|
||||
}
|
||||
|
||||
return E.right(data.right)
|
||||
}
|
||||
|
||||
@@ -164,11 +164,10 @@ export function getFoldersByPath(
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.folders
|
||||
} else {
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
|
||||
return currentCollection.folders
|
||||
|
||||
@@ -63,11 +63,10 @@ export function getRequestsByPath(
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.requests
|
||||
} else {
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
|
||||
return currentCollection.requests
|
||||
|
||||
@@ -51,7 +51,7 @@ const getMethodByDeduction = (parsedArguments: parser.Arguments) => {
|
||||
)(parsedArguments)
|
||||
)
|
||||
return O.some("POST")
|
||||
else return O.none
|
||||
return O.none
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,8 @@ const linter: LinterDefinition = (text) => {
|
||||
severity: "error",
|
||||
},
|
||||
])
|
||||
} else {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
export default linter
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import axios from "axios"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
/**
|
||||
* Create an gist on GitHub with the collection JSON
|
||||
* @param collectionJSON - JSON string of the collection
|
||||
* @param accessToken - GitHub access token
|
||||
* @returns Either of the response of the GitHub Gist API or the error
|
||||
*/
|
||||
export const createCollectionGists = (
|
||||
collectionJSON: string,
|
||||
export const createGist = (
|
||||
content: string,
|
||||
filename: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
return TE.tryCatch(
|
||||
@@ -17,8 +12,8 @@ export const createCollectionGists = (
|
||||
"https://api.github.com/gists",
|
||||
{
|
||||
files: {
|
||||
"hoppscotch-collections.json": {
|
||||
content: collectionJSON,
|
||||
[filename]: {
|
||||
content: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
export const environmentsExporter = (myEnvironments: Environment[]) => {
|
||||
return JSON.stringify(myEnvironments, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import { createGist } from "~/helpers/gist"
|
||||
|
||||
export const environmentsGistExporter = async (
|
||||
environmentsJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
const res = await createGist(
|
||||
environmentsJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(res.right.data.html_url as string)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createGist } from "~/helpers/gist"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export const collectionsGistExporter = async (
|
||||
collectionJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
if (!accessToken) {
|
||||
return E.left("Invalid User")
|
||||
}
|
||||
|
||||
const res = await createGist(
|
||||
collectionJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(true)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
|
||||
export const gqlCollectionsExporter = (
|
||||
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||
) => {
|
||||
return JSON.stringify(gqlCollections, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import { createGist } from "~/helpers/gist"
|
||||
|
||||
export const gqlCollectionsGistExporter = async (
|
||||
gqlCollectionsJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
const res = await createGist(
|
||||
gqlCollectionsJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(res.right.data.html_url as string)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +1,32 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export type HoppExporter<T> = (content: T) => TE.TaskEither<string, string>
|
||||
/**
|
||||
* Create a downloadable file from a collection and prompts the user to download it.
|
||||
* @param collectionJSON - JSON string of the collection
|
||||
* @param name - Name of the collection set as the file name
|
||||
*/
|
||||
export const initializeDownloadCollection = (
|
||||
collectionJSON: string,
|
||||
name: string | null
|
||||
) => {
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
export type HoppExporterDefinition<T> = {
|
||||
name: string
|
||||
exporter: () => Promise<HoppExporter<T>>
|
||||
if (name) {
|
||||
a.download = `${name}.json`
|
||||
} else {
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
}
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
return E.right("state.download_started")
|
||||
}
|
||||
|
||||
export const RESTCollectionExporters: HoppExporterDefinition<
|
||||
HoppCollection<HoppRESTRequest>
|
||||
>[] = [
|
||||
{
|
||||
name: "Hoppscotch REST Collection JSON",
|
||||
exporter: () => import("./hopp").then((m) => m.default),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
|
||||
export const myCollectionsExporter = (
|
||||
myCollections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
return JSON.stringify(myCollections, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||
|
||||
export const teamCollectionsExporter = (teamID: string) => {
|
||||
return getTeamCollectionJSON(teamID)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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)
|
||||
),
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
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"
|
||||
@@ -9,38 +8,23 @@ import {
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { isPlainObject as _isPlainObject } from "lodash-es"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { 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(validateCollection),
|
||||
O.sequenceArray,
|
||||
O.map(RA.toArray)
|
||||
)
|
||||
),
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
export const hoppRESTImporter = (content: string) =>
|
||||
pipe(
|
||||
safeParseJSON(content),
|
||||
O.chain(
|
||||
flow(
|
||||
makeCollectionsArray,
|
||||
RA.map(validateCollection),
|
||||
O.sequenceArray,
|
||||
O.map(RA.toArray)
|
||||
)
|
||||
),
|
||||
})
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
/**
|
||||
* checks if a value is a plain object
|
||||
@@ -63,9 +47,8 @@ const isValidCollection = (
|
||||
const validateCollection = (collection: unknown) => {
|
||||
if (isValidCollection(collection)) {
|
||||
return O.some(collection)
|
||||
} else {
|
||||
return O.some(translateToNewRESTCollection(collection))
|
||||
}
|
||||
return O.some(translateToNewRESTCollection(collection))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
const hoppEnvSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const hoppEnvImporter = (content: string) => {
|
||||
const parsedContent = safeParseJSON(content)
|
||||
|
||||
// parse json from the environments string
|
||||
if (O.isNone(parsedContent)) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const environments = validationResult.data
|
||||
|
||||
return TE.right(environments)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
// TODO: add zod validation
|
||||
export const hoppGqlCollectionsImporter = (
|
||||
content: string
|
||||
): E.Either<"INVALID_JSON", HoppCollection<HoppGQLRequest>[]> => {
|
||||
return E.tryCatch(
|
||||
() => JSON.parse(content) as HoppCollection<HoppGQLRequest>[],
|
||||
() => "INVALID_JSON"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function FileSource(metadata: {
|
||||
acceptedFileTypes: string
|
||||
caption: string
|
||||
onImportFromFile: (content: string) => any | Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, FileImportVue, () => ({
|
||||
acceptedFileTypes: metadata.acceptedFileTypes,
|
||||
caption: metadata.caption,
|
||||
onImportFromFile: metadata.onImportFromFile,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import axios from "axios"
|
||||
import UrlImport from "~/components/importExport/ImportExportSteps/UrlImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { z } from "zod"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function GistSource(metadata: {
|
||||
caption: string
|
||||
onImportFromGist: (
|
||||
importResult: E.Either<string, string>
|
||||
) => any | Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, UrlImport, () => ({
|
||||
caption: metadata.caption,
|
||||
onImportFromURL: (gistResponse) => {
|
||||
const fileSchema = z.object({
|
||||
files: z.record(z.object({ content: z.string() })),
|
||||
})
|
||||
|
||||
const parseResult = fileSchema.safeParse(gistResponse)
|
||||
|
||||
if (!parseResult.success) {
|
||||
metadata.onImportFromGist(E.left("INVALID_GIST"))
|
||||
return
|
||||
}
|
||||
|
||||
const content = Object.values(parseResult.data.files)[0].content
|
||||
|
||||
metadata.onImportFromGist(E.right(content))
|
||||
},
|
||||
fetchLogic: fetchGistFromUrl,
|
||||
}))
|
||||
}
|
||||
|
||||
const fetchGistFromUrl = (url: string) =>
|
||||
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import UrlImport from "~/components/importExport/ImportExportSteps/UrlImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function UrlSource(metadata: {
|
||||
caption: string
|
||||
onImportFromURL: (content: string) => any | Promise<any>
|
||||
fetchLogic?: (url: string) => Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, UrlImport, () => ({
|
||||
caption: metadata.caption,
|
||||
onImportFromURL: (content: unknown) => {
|
||||
if (typeof content === "string") {
|
||||
metadata.onImportFromURL(content)
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -1,22 +1,5 @@
|
||||
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
|
||||
export { hoppRESTImporter } from "./hopp"
|
||||
export { hoppOpenAPIImporter } from "./openapi"
|
||||
export { hoppPostmanImporter } from "./postman"
|
||||
export { hoppInsomniaImporter } from "./insomnia"
|
||||
export { toTeamsImporter } from "./myCollections"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
import { convert, ImportRequest } from "insomnia-importers"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
@@ -16,8 +15,7 @@ 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 "."
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
|
||||
|
||||
@@ -210,27 +208,10 @@ const getHoppCollections = (doc: InsomniaDoc) =>
|
||||
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)
|
||||
),
|
||||
})
|
||||
export const hoppInsomniaImporter = (fileContent: string) =>
|
||||
pipe(
|
||||
fileContent,
|
||||
parseInsomniaDoc,
|
||||
TO.map(getHoppCollections),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
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"
|
||||
import { importJSONToTeam } from "~/helpers/backend/mutations/TeamCollection"
|
||||
|
||||
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),
|
||||
})
|
||||
export function toTeamsImporter(content: string, teamID: string) {
|
||||
return importJSONToTeam(content, teamID)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconOpenAPI from "~icons/lucide/file"
|
||||
import {
|
||||
OpenAPI,
|
||||
OpenAPIV2,
|
||||
@@ -25,8 +24,7 @@ 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 "."
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||
|
||||
@@ -167,14 +165,12 @@ const parseOpenAPIV3BodyFormData = (
|
||||
contentType,
|
||||
body: keys.map((key) => `${key}: `).join("\n"),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) =>
|
||||
<FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) => <FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +229,9 @@ const resolveOpenAPIV3SecurityObj = (
|
||||
} else if (scheme.scheme === "bearer") {
|
||||
// Bearer
|
||||
return { authType: "bearer", authActive: true, token: "" }
|
||||
} else {
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
} else if (scheme.type === "apiKey") {
|
||||
if (scheme.in === "header") {
|
||||
return {
|
||||
@@ -301,17 +296,16 @@ const resolveOpenAPIV3SecurityObj = (
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.type === "openIdConnect") {
|
||||
return {
|
||||
@@ -339,7 +333,7 @@ const resolveOpenAPIV3SecurityScheme = (
|
||||
| undefined
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
else return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3Security = (
|
||||
@@ -439,17 +433,16 @@ const resolveOpenAPIV2SecurityScheme = (
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,44 +607,29 @@ const parseOpenAPIDocContent = (str: string) =>
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
export const hoppOpenAPIImporter = (fileContent: string) =>
|
||||
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)
|
||||
)
|
||||
),
|
||||
})
|
||||
// Deference the references
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.dereference(obj),
|
||||
() => OPENAPI_DEREF_ERROR
|
||||
)
|
||||
)
|
||||
),
|
||||
TE.chainW(convertOpenApiDocToHopp)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import {
|
||||
Collection as PMCollection,
|
||||
Item,
|
||||
@@ -25,8 +24,7 @@ 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 { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { PMRawLanguage } from "~/types/pm-coll-exts"
|
||||
import { stringArrayJoin } from "~/helpers/functional/array"
|
||||
|
||||
@@ -298,28 +296,13 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
|
||||
|
||||
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,
|
||||
export const hoppPostmanImporter = (fileContent: string) =>
|
||||
pipe(
|
||||
// Try reading
|
||||
fileContent,
|
||||
readPMCollection,
|
||||
|
||||
O.map(flow(getHoppCollection, A.of)),
|
||||
O.map(flow(getHoppCollection, A.of)),
|
||||
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
const postmanEnvSchema = z.object({
|
||||
name: z.string(),
|
||||
values: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const postmanEnvImporter = (content: string) => {
|
||||
const parsedContent = safeParseJSON(content)
|
||||
|
||||
// parse json from the environments string
|
||||
if (O.isNone(parsedContent)) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = postmanEnvSchema.safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const postmanEnv = validationResult.data
|
||||
|
||||
const environment: Environment = {
|
||||
name: postmanEnv.name,
|
||||
variables: [],
|
||||
}
|
||||
|
||||
postmanEnv.values.forEach(({ key, value }) =>
|
||||
environment.variables.push({ key, value })
|
||||
)
|
||||
|
||||
return TE.right(environment)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
@@ -62,27 +62,25 @@ const buildHarPostParams = (
|
||||
),
|
||||
RA.toArray
|
||||
)
|
||||
} else {
|
||||
// FormData has its own format
|
||||
return req.body.body.flatMap((entry) => {
|
||||
if (entry.isFile) {
|
||||
// We support multiple files
|
||||
return entry.value.map(
|
||||
(file) =>
|
||||
<Har.Param>{
|
||||
name: entry.key,
|
||||
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
|
||||
contentType: file.type,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
name: entry.key,
|
||||
value: entry.value,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// FormData has its own format
|
||||
return req.body.body.flatMap((entry) => {
|
||||
if (entry.isFile) {
|
||||
// We support multiple files
|
||||
return entry.value.map(
|
||||
(file) =>
|
||||
<Har.Param>{
|
||||
name: entry.key,
|
||||
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
|
||||
contentType: file.type,
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
name: entry.key,
|
||||
value: entry.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const buildHarPostData = (req: HoppRESTRequest): Har.PostData | undefined => {
|
||||
|
||||
@@ -319,7 +319,7 @@ export default class NewTeamCollectionAdapter {
|
||||
|
||||
if (!parentCollection) return
|
||||
|
||||
if (parentCollection.children != null) {
|
||||
if (parentCollection.children !== null) {
|
||||
parentCollection.children.push(collection)
|
||||
} else {
|
||||
parentCollection.children = [collection]
|
||||
@@ -997,7 +997,7 @@ export default class NewTeamCollectionAdapter {
|
||||
|
||||
if (!collection) return
|
||||
|
||||
if (collection.children != null) return
|
||||
if (collection.children !== null) return
|
||||
|
||||
this.loadingCollections$.next([
|
||||
...this.loadingCollections$.getValue(),
|
||||
|
||||
Reference in New Issue
Block a user