feat: introduces ability to export single environment variables and allow CLI to accept the export format used by the app (#3380)
* feat: add ability to export a single environment * refactor: export environment without id * feat: introducing zod for checking json format for environment variables * refactor: new zod specific type for HoppEnvPair * feat: add ability to export single environment in team environment * refactor: moved zod as a dependency to devDependency * refactor: separated repeating logic to helper file * refactor: removed unnecessary to string operation * chore: rearranged smart item placement * refactor: introduced error type when a bulk environment export is used in cli * refactor: removed unnecssary type exports and updated logic and variable names across most files * refactor: better logic for type shapes * chore: bump hoppscotch-cli package version --------- Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2694731c36
commit
185c225297
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
@@ -55,6 +55,7 @@
|
||||
"qs": "^6.10.3",
|
||||
"ts-jest": "^27.1.4",
|
||||
"tsup": "^5.12.7",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "^4.6.4",
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
ERROR_MSG = `Unavailable command: ${error.command}`;
|
||||
break;
|
||||
case "MALFORMED_ENV_FILE":
|
||||
ERROR_MSG = `The environment file is not of the correct format.`;
|
||||
break;
|
||||
case "BULK_ENV_FILE":
|
||||
ERROR_MSG = `CLI doesn't support bulk environments export.`;
|
||||
break;
|
||||
case "MALFORMED_COLLECTION":
|
||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||
break;
|
||||
@@ -82,4 +87,4 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
console.error(ERROR_CODE, ERROR_MSG);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
import { error } from "../../types/errors";
|
||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||
import {
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path)
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.safeParse(contents);
|
||||
|
||||
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||
// CLI doesnt support bulk environments export.
|
||||
// Hence we check for this case and throw an error if it matches the format.
|
||||
if (HoppBulkEnvExportObjectResult.success) {
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
const envPairs: Array<HoppEnvPair> = []
|
||||
// Checks if the environment file is of the correct format.
|
||||
// If it doesnt match either of them, we throw an error.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
for( const [key,value] of Object.entries(contents)) {
|
||||
if(typeof value !== "string") {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
|
||||
envPairs.push({key, value})
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type HoppErrors = {
|
||||
REQUEST_ERROR: HoppErrorData;
|
||||
INVALID_ARGUMENT: HoppErrorData;
|
||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
INVALID_FILE_TYPE: HoppErrorData;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
@@ -9,6 +10,22 @@ export type FormDataEntry = {
|
||||
|
||||
export type HoppEnvPair = { key: string; value: string };
|
||||
|
||||
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||
|
||||
// Shape of the single environment export object that is exported from the app.
|
||||
export const HoppEnvExportObject = z.object({
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app.
|
||||
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
||||
|
||||
export type HoppEnvs = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
|
||||
@@ -746,6 +746,7 @@
|
||||
"disconnected_from": "Disconnected from {name}",
|
||||
"docs_generated": "Documentation generated",
|
||||
"download_started": "Download started",
|
||||
"download_failed": "Download failed",
|
||||
"enabled": "Enabled",
|
||||
"file_imported": "File imported",
|
||||
"finished_in": "Finished in {duration} ms",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
role="menu"
|
||||
@keyup.e="edit!.$el.click()"
|
||||
@keyup.d="duplicate!.$el.click()"
|
||||
@keyup.j="exportAsJsonEl!.$el.click()"
|
||||
@keyup.delete="
|
||||
!(environmentIndex === 'Global')
|
||||
? deleteAction!.$el.click()
|
||||
@@ -77,6 +78,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
:shortcut="['J']"
|
||||
@click="
|
||||
() => {
|
||||
exportEnvironmentAsJSON()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="environmentIndex !== 'Global'"
|
||||
ref="deleteAction"
|
||||
@@ -121,6 +134,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -136,10 +150,18 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () => {
|
||||
const { environment, environmentIndex } = props
|
||||
exportAsJSON(environment, environmentIndex)
|
||||
? toast.success(t("state.download_started"))
|
||||
: toast.error(t("state.download_failed"))
|
||||
}
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const exportAsJsonEl = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
role="menu"
|
||||
@keyup.e="edit!.$el.click()"
|
||||
@keyup.d="duplicate!.$el.click()"
|
||||
@keyup.j="exportAsJsonEl!.$el.click()"
|
||||
@keyup.delete="deleteAction!.$el.click()"
|
||||
@keyup.escape="options!.tippy().hide()"
|
||||
>
|
||||
@@ -54,6 +55,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
@@ -66,6 +68,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
:shortcut="['J']"
|
||||
@click="
|
||||
() => {
|
||||
exportEnvironmentAsJSON()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
@@ -109,6 +123,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -124,11 +139,17 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () =>
|
||||
exportAsJSON(props.environment)
|
||||
? toast.success(t("state.download_started"))
|
||||
: toast.error(t("state.download_failed"))
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
const exportAsJsonEl = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
pipe(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
const getEnvironmentJson = (
|
||||
environmentObj: TeamEnvironment | Environment,
|
||||
environmentIndex?: number | "Global" | null
|
||||
) => {
|
||||
const newEnvironment =
|
||||
"environment" in environmentObj
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
: environmentObj.id
|
||||
|
||||
return environmentId !== null
|
||||
? JSON.stringify(newEnvironment, null, 2)
|
||||
: undefined
|
||||
}
|
||||
|
||||
export const exportAsJSON = (
|
||||
environmentObj: Environment | TeamEnvironment,
|
||||
environmentIndex?: number | "Global" | null
|
||||
): boolean => {
|
||||
const dataToWrite = getEnvironmentJson(environmentObj, environmentIndex)
|
||||
|
||||
if (!dataToWrite) return false
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
return true
|
||||
}
|
||||
@@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
|
||||
Reference in New Issue
Block a user