Browse Source

feat: import environments from insomnia (#3625)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
Akash K 1 year ago
parent
commit
a8cc569786

+ 3 - 2
packages/hoppscotch-common/locales/en.json

@@ -392,7 +392,8 @@
     "hoppscotch_environment": "Hoppscotch Environment",
     "hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
     "postman_environment": "Postman Environment",
-    "postman_environment_description": "Import Postman Environment JSON file",
+    "postman_environment_description": "Import Postman Environment from a JSON file",
+    "insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
     "environments_from_gist": "Import From Gist",
     "environments_from_gist_description": "Import Hoppscotch Environments From Gist",
     "gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
@@ -968,4 +969,4 @@
     "team": "Team Workspace",
     "title": "Workspaces"
   }
-}
+}

+ 64 - 2
packages/hoppscotch-common/src/components/environments/ImportExport.vue

@@ -18,16 +18,22 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
 import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
 
 import * as E from "fp-ts/Either"
-import { appendEnvironments, environments$ } from "~/newstore/environments"
+import {
+  appendEnvironments,
+  addGlobalEnvVariable,
+  environments$,
+} from "~/newstore/environments"
 
 import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
 import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
 import { GQLError } from "~/helpers/backend/GQLClient"
 import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
 import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
+import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv"
 
 import IconFolderPlus from "~icons/lucide/folder-plus"
 import IconPostman from "~icons/hopp/postman"
+import IconInsomnia from "~icons/hopp/insomnia"
 import IconUser from "~icons/lucide/user"
 import { initializeDownloadCollection } from "~/helpers/import-export/export"
 import { computed } from "vue"
@@ -136,6 +142,51 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
   }),
 }
 
+const insomniaEnvironmentsImport: ImporterOrExporter = {
+  metadata: {
+    id: "import.from_insomnia",
+    name: "import.from_insomnia",
+    icon: IconInsomnia,
+    title: "import.from_json",
+    applicableTo: ["personal-workspace", "team-workspace"],
+    disabled: false,
+  },
+  component: FileSource({
+    acceptedFileTypes: "application/json",
+    caption: "import.insomnia_environment_description",
+    onImportFromFile: async (environments) => {
+      const res = await insomniaEnvImporter(environments)()
+
+      if (E.isLeft(res)) {
+        showImportFailedError()
+        return
+      }
+
+      const globalEnvIndex = res.right.findIndex(
+        (env) => env.name === "Base Environment"
+      )
+
+      const globalEnv =
+        globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
+
+      // remove the global env from the environments array to prevent it from being imported twice
+      if (globalEnvIndex !== -1) {
+        res.right.splice(globalEnvIndex, 1)
+      }
+
+      handleImportToStore(res.right, globalEnv)
+
+      platform.analytics?.logEvent({
+        type: "HOPP_IMPORT_ENVIRONMENT",
+        platform: "rest",
+        workspaceType: isTeamEnvironment.value ? "team" : "personal",
+      })
+
+      emit("hide-modal")
+    },
+  }),
+}
+
 const EnvironmentsImportFromGIST: ImporterOrExporter = {
   metadata: {
     id: "import.environments_from_gist",
@@ -255,6 +306,7 @@ const importerModules = [
   HoppEnvironmentsImport,
   EnvironmentsImportFromGIST,
   PostmanEnvironmentsImport,
+  insomniaEnvironmentsImport,
 ]
 
 const exporterModules = computed(() => {
@@ -271,7 +323,17 @@ const showImportFailedError = () => {
   toast.error(t("import.failed").toString())
 }
 
-const handleImportToStore = async (environments: Environment[]) => {
+const handleImportToStore = async (
+  environments: Environment[],
+  globalEnv?: Environment
+) => {
+  // if there's a global env, add them to the store
+  if (globalEnv) {
+    globalEnv.variables.forEach(({ key, value }) => {
+      addGlobalEnvVariable({ key, value })
+    })
+  }
+
   if (props.environmentType === "MY_ENV") {
     appendEnvironments(environments)
     toast.success(t("state.file_imported"))

+ 16 - 0
packages/hoppscotch-common/src/helpers/functional/yaml.ts

@@ -0,0 +1,16 @@
+import yaml from "js-yaml"
+import * as O from "fp-ts/Option"
+import { safeParseJSON } from "./json"
+import { pipe } from "fp-ts/function"
+
+export const safeParseYAML = (str: string) => O.tryCatch(() => yaml.load(str))
+
+export const safeParseJSONOrYAML = (str: string) =>
+  pipe(
+    str,
+    safeParseJSON,
+    O.match(
+      () => safeParseYAML(str),
+      (data) => O.of(data)
+    )
+  )

+ 4 - 6
packages/hoppscotch-common/src/helpers/import-export/import/insomnia.ts

@@ -1,5 +1,5 @@
 import { convert, ImportRequest } from "insomnia-importers"
-import { pipe, flow } from "fp-ts/function"
+import { pipe } from "fp-ts/function"
 import {
   HoppRESTAuth,
   HoppRESTHeader,
@@ -12,10 +12,10 @@ import {
   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 { IMPORTER_INVALID_FILE_FORMAT } from "."
+import { replaceInsomniaTemplating } from "./insomniaEnv"
 
 // TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
 
@@ -32,10 +32,8 @@ 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 replaceVarTemplating = (expression: string) =>
+  replaceInsomniaTemplating(expression)
 
 const getFoldersIn = (
   folder: InsomniaFolderResource | null,

+ 85 - 0
packages/hoppscotch-common/src/helpers/import-export/import/insomniaEnv.ts

@@ -0,0 +1,85 @@
+import * as TE from "fp-ts/TaskEither"
+import * as O from "fp-ts/Option"
+
+import { IMPORTER_INVALID_FILE_FORMAT } from "."
+
+import { z } from "zod"
+import { Environment } from "@hoppscotch/data"
+import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
+
+const insomniaResourcesSchema = z.object({
+  resources: z.array(
+    z
+      .object({
+        _type: z.string(),
+      })
+      .passthrough()
+  ),
+})
+
+const insomniaEnvSchema = z.object({
+  _type: z.literal("environment"),
+  name: z.string(),
+  data: z.record(z.string()),
+})
+
+export const replaceInsomniaTemplating = (expression: string) => {
+  const regex = /\{\{ _\.([^}]+) \}\}/g
+  return expression.replaceAll(regex, "<<$1>>")
+}
+
+export const insomniaEnvImporter = (content: string) => {
+  const parsedContent = safeParseJSONOrYAML(content)
+
+  if (O.isNone(parsedContent)) {
+    return TE.left(IMPORTER_INVALID_FILE_FORMAT)
+  }
+
+  const validationResult = insomniaResourcesSchema.safeParse(
+    parsedContent.value
+  )
+
+  if (!validationResult.success) {
+    return TE.left(IMPORTER_INVALID_FILE_FORMAT)
+  }
+
+  const insomniaEnvs = validationResult.data.resources
+    .filter((resource) => resource._type === "environment")
+    .map((envResource) => {
+      const envResourceData = envResource.data as Record<string, unknown>
+      const stringifiedData: Record<string, string> = {}
+
+      Object.keys(envResourceData).forEach((key) => {
+        stringifiedData[key] = String(envResourceData[key])
+      })
+
+      return { ...envResource, data: stringifiedData }
+    })
+
+  const environments: Environment[] = []
+
+  insomniaEnvs.forEach((insomniaEnv) => {
+    const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
+
+    if (parsedInsomniaEnv.success) {
+      const environment: Environment = {
+        name: parsedInsomniaEnv.data.name,
+        variables: Object.entries(parsedInsomniaEnv.data.data).map(
+          ([key, value]) => ({ key, value })
+        ),
+      }
+
+      environments.push(environment)
+    }
+  })
+
+  const processedEnvironments = environments.map((env) => ({
+    ...env,
+    variables: env.variables.map((variable) => ({
+      ...variable,
+      value: replaceInsomniaTemplating(variable.value),
+    })),
+  }))
+
+  return TE.right(processedEnvironments)
+}