diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index eeaf86ade..f1764c887 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/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" } -} +} \ No newline at end of file diff --git a/packages/hoppscotch-common/src/components/environments/ImportExport.vue b/packages/hoppscotch-common/src/components/environments/ImportExport.vue index 3c9162fe7..3bbe7d32a 100644 --- a/packages/hoppscotch-common/src/components/environments/ImportExport.vue +++ b/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")) diff --git a/packages/hoppscotch-common/src/helpers/functional/yaml.ts b/packages/hoppscotch-common/src/helpers/functional/yaml.ts new file mode 100644 index 000000000..76db0e7d5 --- /dev/null +++ b/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) + ) + ) diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/insomnia.ts b/packages/hoppscotch-common/src/helpers/import-export/import/insomnia.ts index 2ecca47d5..c080a54a6 100644 --- a/packages/hoppscotch-common/src/helpers/import-export/import/insomnia.ts +++ b/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, diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/insomniaEnv.ts b/packages/hoppscotch-common/src/helpers/import-export/import/insomniaEnv.ts new file mode 100644 index 000000000..19c743109 --- /dev/null +++ b/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 + const stringifiedData: Record = {} + + 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) +}