feat: initial and current value for environment variables (#5055)

This commit is contained in:
Nivedin 2025-05-23 22:38:36 +05:30 committed by GitHub
parent e7aba8a914
commit 01d96fa577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1684 additions and 431 deletions

View file

@ -95,6 +95,7 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
const testFixtures = [
{ fileName: "env-v0.json", version: 0 },
{ fileName: "env-v1.json", version: 1 },
{ fileName: "env-v2.json", version: 2 },
];
testFixtures.forEach(({ fileName, version }) => {

View file

@ -1,34 +1,42 @@
{
"v": 1,
"v": 2,
"id": "cm0dsn3v70004p4qk3l9b7sjm",
"name": "AWS Signature - environments",
"variables": [
{
"key": "awsRegion",
"value": "us-east-1",
"currentValue": "us-east-1",
"initialValue": "us-east-1",
"secret": false
},
{
"key": "serviceName",
"value": "s3",
"currentValue": "s3",
"initialValue": "s3",
"secret": false
},
{
"key": "accessKey",
"value": "test-access-key",
"currentValue": "test-access-key",
"initialValue": "test-access-key",
"secret": true
},
{
"key": "secretKey",
"currentValue": "",
"initialValue": "",
"secret": true
},
{
"key": "url",
"value": "https://echo.hoppscotch.io",
"currentValue": "https://echo.hoppscotch.io",
"initialValue": "https://echo.hoppscotch.io",
"secret": false
},
{
"key": "serviceToken",
"currentValue": "",
"initialValue": "",
"secret": true
}
]

View file

@ -1,21 +1,24 @@
{
"v": 1,
"v": 2,
"id": "cm0dsn3v70004p4qk3l9b7sjm",
"name": "Digest Auth - environments",
"variables": [
{
"key": "username",
"value": "admin",
"currentValue": "",
"initialValue": "admin",
"secret": true
},
{
"key": "password",
"value": "admin",
"currentValue": "",
"initialValue": "admin",
"secret": true
},
{
"key": "url",
"value": "https://test.insightres.org/digest/"
"currentValue": "",
"initialValue": "https://test.insightres.org/digest/"
}
]
}

View file

@ -1,10 +1,10 @@
{
"name": "env-v0",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io",
"secret": false
}
]
}
"name": "env-v1",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io",
"secret": false
}
]
}

View file

@ -0,0 +1,13 @@
{
"id": "env-v2",
"v": 2,
"name": "env-v2",
"variables": [
{
"key": "baseURL",
"initialValue": "https://echo.hoppscotch.io",
"currentValue": "https://echo.hoppscotch.io",
"secret": false
}
]
}

View file

@ -1,33 +1,42 @@
{
"v": 1,
"v": 2,
"id": "cm00r7kpb0006mbd2nq1560w6",
"name": "Request variables alongside environment variables",
"variables": [
{
"key": "url",
"value": "https://echo.hoppscotch.io",
"initialValue": "https://echo.hoppscotch.io",
"currentValue": "https://echo.hoppscotch.io",
"secret": false
},
{
"key": "secretBasicAuthPasswordEnvVar",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretBasicAuthUsernameEnvVar",
"value": "username",
"initialValue": "username",
"currentValue": "",
"secret": true
},
{
"key": "username",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "password",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "testHeaderValue",
"value": "test-header-value",
"initialValue": "test-header-value",
"currentValue": "",
"secret": false
}
]

View file

@ -1,26 +1,36 @@
{
"v": 1,
"v": 2,
"id": "2",
"name": "secret-envs-persistence-scripting-envs",
"variables": [
{
"key": "preReqVarOne",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "preReqVarTwo",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "postReqVarOne",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "preReqVarTwo",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "customHeaderValueFromSecretVar",
"initialValue": "",
"currentValue": "",
"secret": true
}
]

View file

@ -1,44 +1,60 @@
{
"id": "2",
"v": 1,
"v": 2,
"name": "secret-envs",
"variables": [
{
"key": "secretBearerToken",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretQueryParamValue",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretBodyValue",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "secretHeaderValue",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "echoHoppBaseURL",
"value": "https://echo.hoppscotch.io",
"initialValue": "https://echo.hoppscotch.io",
"currentValue": "",
"secret": false
},
{
"key": "httpbinBaseURL",
"value": "https://httpbin.org",
"initialValue": "https://httpbin.org",
"currentValue": "",
"secret": false
}
]

View file

@ -1,50 +1,60 @@
{
"v": 1,
"v": 2,
"id": "2",
"name": "secret-values-envs",
"variables": [
{
"key": "secretBearerToken",
"value": "test-token",
"initialValue": "test-token",
"currentValue": "test-token",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"value": "test-user",
"initialValue": "test-user",
"currentValue": "test-user",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"value": "test-pass",
"initialValue": "test-pass",
"currentValue": "test-pass",
"secret": true
},
{
"key": "secretQueryParamValue",
"value": "secret-query-param-value",
"initialValue": "secret-query-param-value",
"currentValue": "secret-query-param-value",
"secret": true
},
{
"key": "secretBodyValue",
"value": "secret-body-value",
"initialValue": "secret-body-value",
"currentValue": "secret-body-value",
"secret": true
},
{
"key": "secretHeaderValue",
"value": "secret-header-value",
"initialValue": "secret-header-value",
"currentValue": "secret-header-value",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"initialValue": "",
"currentValue": "",
"secret": true
},
{
"key": "echoHoppBaseURL",
"value": "https://echo.hoppscotch.io",
"initialValue": "https://echo.hoppscotch.io",
"currentValue": "https://echo.hoppscotch.io",
"secret": false
},
{
"key": "httpbinBaseURL",
"value": "https://httpbin.org",
"initialValue": "https://httpbin.org",
"currentValue": "https://httpbin.org",
"secret": false
}
]

View file

@ -44,7 +44,12 @@ describe("getters", () => {
describe("getEffectiveFinalMetaData", () => {
const environmentVariables = [
{ key: "PARAM", value: "parsed_param", secret: false },
{
key: "PARAM",
initialValue: "parsed_param",
currentValue: "parsed_param",
secret: false,
},
];
test("Empty list of meta-data", () => {
@ -421,27 +426,32 @@ describe("getters", () => {
const environmentVariables = [
{
key: "SHARED_KEY_I",
value: "environment-variable-shared-value-I",
initialValue: "environment-variable-shared-value-I",
currentValue: "environment-variable-shared-value-I",
secret: false,
},
{
key: "SHARED_KEY_II",
value: "environment-variable-shared-value-II",
initialValue: "environment-variable-shared-value-II",
currentValue: "environment-variable-shared-value-II",
secret: false,
},
{
key: "ENV_VAR_III",
value: "environment-variable-value-III",
initialValue: "environment-variable-value-III",
currentValue: "environment-variable-value-III",
secret: false,
},
{
key: "ENV_VAR_IV",
value: "environment-variable-value-IV",
initialValue: "environment-variable-value-IV",
currentValue: "environment-variable-value-IV",
secret: false,
},
{
key: "ENV_VAR_V",
value: "environment-variable-value-V",
initialValue: "environment-variable-value-V",
currentValue: "environment-variable-value-V",
secret: false,
},
];
@ -450,32 +460,38 @@ describe("getters", () => {
const expected = [
{
key: "SHARED_KEY_I",
value: "request-variable-shared-value-I",
currentValue: "request-variable-shared-value-I",
initialValue: "request-variable-shared-value-I",
secret: false,
},
{
key: "REQUEST_VAR_III",
value: "request-variable-value-III",
currentValue: "request-variable-value-III",
initialValue: "request-variable-value-III",
secret: false,
},
{
key: "SHARED_KEY_II",
value: "environment-variable-shared-value-II",
currentValue: "environment-variable-shared-value-II",
initialValue: "environment-variable-shared-value-II",
secret: false,
},
{
key: "ENV_VAR_III",
value: "environment-variable-value-III",
currentValue: "environment-variable-value-III",
initialValue: "environment-variable-value-III",
secret: false,
},
{
key: "ENV_VAR_IV",
value: "environment-variable-value-IV",
currentValue: "environment-variable-value-IV",
initialValue: "environment-variable-value-IV",
secret: false,
},
{
key: "ENV_VAR_V",
value: "environment-variable-value-V",
currentValue: "environment-variable-value-V",
initialValue: "environment-variable-value-V",
secret: false,
},
];

View file

@ -75,7 +75,8 @@ export const test = (pathOrId: string, options: TestCmdOptions) => async () => {
(key) =>
<IterationDataItem>{
key: key,
value: iterationDataItem[key],
initialValue: iterationDataItem[key],
currentValue: iterationDataItem[key],
secret: false,
}
)

View file

@ -60,11 +60,16 @@ export async function parseEnvsData(options: TestCmdEnvironmentOptions) {
if (HoppEnvKeyPairResult.success) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value, secret: false });
envPairs.push({
key,
initialValue: value,
currentValue: value,
secret: false,
});
}
} else if (HoppEnvExportObjectResult.type === "ok") {
// Original environment variables from the supplied export file
const originalEnvVariables = (contents as NonSecretEnvironment).variables;
const originalEnvVariables = (contents as Environment).variables;
// Above environment variables conforming to the latest schema
// `value` fields if specified will be omitted for secret environment variables
@ -73,10 +78,10 @@ export async function parseEnvsData(options: TestCmdEnvironmentOptions) {
// The values supplied for secret environment variables have to be considered in the CLI
// For each secret environment variable, include the value in case supplied
const resolvedEnvVariables = migratedEnvVariables.map((variable, idx) => {
if (variable.secret && originalEnvVariables[idx].value) {
if (variable.secret && originalEnvVariables[idx].initialValue) {
return {
...variable,
value: originalEnvVariables[idx].value,
initialValue: originalEnvVariables[idx].initialValue,
};
}

View file

@ -275,9 +275,15 @@ export const getResolvedVariables = (
requestVariables: HoppRESTRequestVariables,
environmentVariables: EnvironmentVariable[]
): EnvironmentVariable[] => {
// Transforming request variables to the shape of environment variables
const activeRequestVariables = requestVariables
.filter(({ active, value }) => active && value)
.map(({ key, value }) => ({ key, value, secret: false }));
.map(({ key, value }) => ({
key,
initialValue: value,
currentValue: value,
secret: false,
}));
const requestVariableKeys = activeRequestVariables.map(({ key }) => key);
@ -286,5 +292,17 @@ export const getResolvedVariables = (
({ key }) => !requestVariableKeys.includes(key)
);
return [...activeRequestVariables, ...filteredEnvironmentVariables];
// Setting currentValue to initialValue for environment variables
// because the exported file might not have the currentValue field
const processedEnvironmentVariables = filteredEnvironmentVariables.map(
({ key, initialValue, currentValue, secret }) => ({
key,
initialValue,
currentValue:
currentValue && currentValue !== "" ? currentValue : initialValue,
secret,
})
);
return [...activeRequestVariables, ...processedEnvironmentVariables];
};

View file

@ -42,8 +42,10 @@ const processVariables = (variable: Environment["variables"][number]) => {
if (variable.secret) {
return {
...variable,
value:
"value" in variable ? variable.value : process.env[variable.key] || "",
currentValue:
"currentValue" in variable && variable.currentValue !== ""
? variable.currentValue
: process.env[variable.key] || "",
};
}
return variable;

View file

@ -340,11 +340,13 @@
}
},
"count": {
"currentValue": "Current value {count}",
"description": "Description {count}",
"header": "Header {count}",
"initialValue": "Initial value {count}",
"key": "Key {count}",
"message": "Message {count}",
"parameter": "Parameter {count}",
"key": "Key {count}",
"description": "Description {count}",
"protocol": "Protocol {count}",
"value": "Value {count}",
"variable": "Variable {count}"

View file

@ -70,7 +70,6 @@
</template>
<script lang="ts" setup>
import { Environment } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
@ -80,17 +79,20 @@ import { useToast } from "~/composables/toast"
import { GQLError } from "~/helpers/backend/GQLClient"
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { getEnvActionErrorMessage } from "~/helpers/error-messages"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import {
addEnvironmentVariable,
addGlobalEnvVariable,
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { RESTTabService } from "~/services/tab/rest"
import { Scope } from "./Selector.vue"
import { GlobalEnvironment } from "@hoppscotch/data"
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const currentEnvironmentValueService = useService(CurrentValueService)
const props = defineProps<{
show: boolean
@ -113,6 +115,7 @@ watch(
if (!newVal) {
scope.value = {
type: "global",
variables: [],
}
replaceWithVariable.value = false
editingName.value = ""
@ -123,22 +126,9 @@ watch(
}
)
type Scope =
| {
type: "global"
}
| {
type: "my-environment"
environment: Environment
index: number
}
| {
type: "team-environment"
environment: TeamEnvironment
}
const scope = ref<Scope>({
type: "global",
variables: [],
})
const replaceWithVariable = ref(false)
@ -152,27 +142,67 @@ const addEnvironment = async () => {
return
}
if (scope.value.type === "global") {
addGlobalEnvVariable({
const newVariables = [
...scope.value.variables,
{
key: editingName.value,
initialValue: editingValue.value,
currentValue: "",
secret: false,
},
]
const newEnv: GlobalEnvironment = {
v: 2,
variables: newVariables,
}
setGlobalEnvVariables(newEnv)
currentEnvironmentValueService.addEnvironmentVariable("Global", {
key: editingName.value,
value: editingValue.value,
secret: false,
currentValue: editingValue.value,
isSecret: false,
varIndex: scope.value.variables.length,
})
toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, {
key: editingName.value,
value: editingValue.value,
secret: false,
})
const newVariables = [
...scope.value.environment.variables,
{
key: editingName.value,
initialValue: editingValue.value,
currentValue: "",
secret: false,
},
]
const newEnv = {
...scope.value.environment,
variables: newVariables,
}
updateEnvironment(scope.value.index, newEnv)
currentEnvironmentValueService.addEnvironmentVariable(
scope.value.environment.id,
{
key: editingName.value,
currentValue: editingValue.value,
isSecret: false,
varIndex: scope.value.environment.variables.length,
}
)
toast.success(`${t("environment.updated")}`)
} else {
const newVariables = [
...scope.value.environment.environment.variables,
{
key: editingName.value,
value: editingValue.value,
initialValue: editingValue.value,
currentValue: "",
secret: false,
},
]
await pipe(
updateTeamEnvironment(
JSON.stringify(newVariables),
@ -185,6 +215,18 @@ const addEnvironment = async () => {
toast.error(t(getEnvActionErrorMessage(err)))
},
() => {
if (scope.value.type === "team-environment") {
currentEnvironmentValueService.addEnvironmentVariable(
scope.value.environment.id,
{
key: editingName.value,
currentValue: editingValue.value,
isSecret: false,
varIndex:
scope.value.environment.environment.variables.length - 1,
}
)
}
hideModal()
toast.success(`${t("environment.updated")}`)
}

View file

@ -170,7 +170,7 @@ const insomniaEnvironmentsImport: ImporterOrExporter = {
disabled: false,
},
component: FileSource({
acceptedFileTypes: "application/json",
acceptedFileTypes: ".json, .yaml, .yml",
caption: "import.insomnia_environment_description",
onImportFromFile: async (environments) => {
isInsomniaImporterInProgress.value = true
@ -362,8 +362,8 @@ const handleImportToStore = async (
) => {
// Add global envs to the store
globalEnvs.forEach(({ variables }) => {
variables.forEach(({ key, value, secret }) => {
addGlobalEnvVariable({ key, value, secret })
variables.forEach(({ key, initialValue, currentValue, secret }) => {
addGlobalEnvVariable({ key, initialValue, currentValue, secret })
})
})

View file

@ -66,6 +66,7 @@
() => {
$emit('update:modelValue', {
type: 'global',
variables: globalVals.variables,
})
hide()
}
@ -245,7 +246,7 @@
</span>
</div>
<div
v-for="(variable, index) in globalEnvs.variables"
v-for="(variable, index) in globalEnvs"
:key="index"
class="flex flex-1 space-x-4"
>
@ -255,14 +256,11 @@
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.value }}
{{ variable.currentValue }}
</template>
</span>
</div>
<div
v-if="globalEnvs.variables.length === 0"
class="text-secondaryLight"
>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
{{ t("environment.empty_variables") }}
</div>
</div>
@ -316,7 +314,7 @@
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.value }}
{{ variable.currentValue }}
</template>
</span>
</div>
@ -359,6 +357,7 @@ import {
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { useLocalState } from "~/newstore/localstate"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { WorkspaceService } from "~/services/workspace.service"
import IconCheck from "~icons/lucide/check"
import IconEdit from "~icons/lucide/edit"
@ -366,9 +365,10 @@ import IconEye from "~icons/lucide/eye"
import IconGlobe from "~icons/lucide/globe"
import IconLayers from "~icons/lucide/layers"
type Scope =
export type Scope =
| {
type: "global"
variables: GlobalEnvironment["variables"]
}
| {
type: "my-environment"
@ -403,6 +403,8 @@ const myEnvironments = useReadonlyStream(environments$, [])
const workspaceService = useService(WorkspaceService)
const workspace = workspaceService.currentWorkspace
const currentEnvironmentValueService = useService(CurrentValueService)
// TeamList-Adapter
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
@ -577,6 +579,7 @@ const selectedEnv = computed(() => {
index: props.modelValue.index,
name: props.modelValue.environment?.name,
variables: props.modelValue.environment?.variables,
id: props.modelValue.environment.id,
}
} else if (props.modelValue?.type === "team-environment") {
return {
@ -584,11 +587,13 @@ const selectedEnv = computed(() => {
name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id,
variables: props.modelValue.environment.environment.variables,
id: props.modelValue.environment.id,
}
}
return {
type: "global",
name: "Global",
variables: globalVals.value.variables,
}
}
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
@ -599,6 +604,7 @@ const selectedEnv = computed(() => {
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
id: environment.id,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
@ -613,6 +619,7 @@ const selectedEnv = computed(() => {
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
id: teamEnv.id,
}
}
return { type: "NO_ENV_SELECTED" }
@ -653,6 +660,7 @@ onMounted(() => {
} else {
emit("update:modelValue", {
type: "global",
variables: globalVals.value.variables,
})
}
}
@ -662,11 +670,29 @@ onMounted(() => {
const envSelectorActions = ref<TippyComponent | null>(null)
const envQuickPeekActions = ref<TippyComponent | null>(null)
const globalEnvs = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const globalVals = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const globalEnvs = computed(() => {
return globalVals.value.variables.map((variable, index) => ({
...variable,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
"Global",
index
) ?? "",
}))
})
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {
return selectedEnv.value.variables
if (selectedEnv.value.variables && selectedEnv.value.id) {
return selectedEnv.value.variables.map((variable, index) => ({
...variable,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
selectedEnv.value.id ?? "",
index
) ?? "",
}))
}
return []
})

View file

@ -104,7 +104,7 @@ const environmentType = ref<EnvironmentsChooseType>({
const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const globalEnvironment = computed<Environment>(() => ({
v: 1 as const,
v: 2 as const,
id: "Global",
name: "Global",
variables: globalEnv.value.variables,
@ -177,6 +177,7 @@ watch(
// if navigating away from a team workspace
if (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
newTeamID &&
selectedEnvironmentIndex.value.teamID !== newTeamID
) {
setSelectedEnvironmentIndex({

View file

@ -3,6 +3,7 @@
v-if="show"
dialog
:title="t(`environment.${action}`)"
styles="sm:max-w-3xl"
@close="hideModal"
>
<template #body>
@ -74,7 +75,7 @@
<template v-else>
<div
v-for="({ id, env }, index) in tab.variables"
:key="`variable-${id}-${index}`"
:key="`${tab.id}-${id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
@ -84,13 +85,23 @@
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"
:name="'param' + index"
:name="'variable' + index"
/>
<SmartEnvInput
v-model="env.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
v-model="env.initialValue"
:placeholder="`${t('count.initialValue', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
:name="'initialValue' + index"
:secret="tab.isSecret"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
/>
<SmartEnvInput
v-model="env.currentValue"
:placeholder="`${t('count.currentValue', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'currentValue' + index"
:secret="tab.isSecret"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
@ -162,6 +173,7 @@ import {
updateEnvironment,
} from "~/newstore/environments"
import { platform } from "~/platform"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import IconDone from "~icons/lucide/check"
import IconHelpCircle from "~icons/lucide/help-circle"
@ -171,11 +183,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
type EnvironmentVariable = {
id: number
env: {
value: string
key: string
secret: boolean
}
env: Environment["variables"][number]
}
const t = useI18n()
@ -237,10 +245,14 @@ const tabsData: ComputedRef<
const editingName = ref<string | null>(null)
const editingID = ref<string>("")
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
{
id: idTicker.value++,
env: { key: "", currentValue: "", initialValue: "", secret: false },
},
])
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
const secretVars = computed(() =>
pipe(
@ -302,8 +314,9 @@ const evnExpandError = computed(() => {
return pipe(
variables,
A.filter(({ secret }) => !secret),
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
A.exists(({ currentValue }) =>
E.isLeft(parseTemplateStringE(currentValue, variables))
)
)
})
@ -314,12 +327,12 @@ const liveEnvs = computed(() => {
if (props.editingEnvironmentIndex === "Global") {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...vars.value.map((x) => ({ ...x.env, sourceEnv: editingName.value! })),
]
}
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalEnv.value.variables.map((x) => ({ ...x, source: "Global" })),
...vars.value.map((x) => ({ ...x.env, sourceEnv: editingName.value! })),
...globalEnv.value.variables.map((x) => ({ ...x, sourceEnv: "Global" })),
]
})
@ -333,6 +346,16 @@ const workingEnvID = computed(() => {
return uniqueID()
})
const getCurrentValue = (id: string | "Global", varIndex: number) => {
const env = workingEnv.value?.variables[varIndex]
if (env && env.secret) {
return secretEnvironmentService.getSecretEnvironmentVariable(id, varIndex)
?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(id, varIndex)
?.currentValue
}
watch(
() => props.show,
(show) => {
@ -351,17 +374,14 @@ watch(
id: idTicker.value++,
env: {
key: e.key,
value: e.secret
? (secretEnvironmentService.getSecretEnvironmentVariable(
props.editingEnvironmentIndex === "Global"
? "Global"
: workingEnvID.value,
index
)?.value ??
// @ts-expect-error `value` field can exist for secret environment variables as inferred while importing
e.value ??
"")
: e.value,
currentValue:
getCurrentValue(
props.editingEnvironmentIndex === "Global"
? "Global"
: workingEnvID.value,
index
) ?? "",
initialValue: e.initialValue,
secret: e.secret,
},
}))
@ -384,7 +404,8 @@ const addEnvironmentVariable = () => {
id: idTicker.value++,
env: {
key: "",
value: "",
currentValue: "",
initialValue: "",
secret: selectedEnvOption.value === "secret",
},
})
@ -421,26 +442,75 @@ const saveEnvironment = () => {
const secretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
e.secret
? O.some({
key: e.key,
value: e.currentValue,
varIndex: i,
})
: O.none
)
)
if (editingID.value) {
secretEnvironmentService.addSecretEnvironment(
editingID.value,
secretVariables
const nonSecretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
!e.secret
? O.some({
key: e.key,
currentValue: e.currentValue,
varIndex: i,
isSecret: e.secret,
})
: O.none
)
} else if (props.editingEnvironmentIndex === "Global") {
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
)
if (secretVariables.length > 0) {
if (editingID.value) {
secretEnvironmentService.addSecretEnvironment(
editingID.value,
secretVariables
)
} else if (props.editingEnvironmentIndex === "Global") {
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
}
}
if (nonSecretVariables.length > 0) {
if (editingID.value) {
currentEnvironmentValueService.addEnvironment(
editingID.value,
nonSecretVariables
)
} else if (props.editingEnvironmentIndex === "Global") {
currentEnvironmentValueService.addEnvironment(
"Global",
nonSecretVariables
)
}
}
const variables = pipe(
filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
A.map((e) =>
e.secret
? {
key: e.key,
secret: e.secret,
initialValue: e.initialValue,
currentValue: "",
}
: {
key: e.key,
secret: e.secret,
initialValue: e.initialValue,
currentValue: "",
}
)
)
const environmentUpdated: Environment = {
v: 1,
v: 2,
id: uniqueID(),
name: editingName.value,
variables,

View file

@ -155,6 +155,7 @@ import IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconTrash2 from "~icons/lucide/trash-2"
import { CurrentValueService } from "~/services/current-environment-value.service"
const t = useI18n()
const toast = useToast()
@ -183,6 +184,7 @@ const emit = defineEmits<{
const confirmRemove = ref(false)
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
watch(
() => props.duplicateGlobalEnvironmentLoading,
@ -216,6 +218,7 @@ const removeEnvironment = () => {
if (!isGlobalEnvironment.value) {
deleteEnvironment(props.environmentIndex as number, props.environment.id)
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
currentEnvironmentValueService.deleteEnvironment(props.environment.id)
}
toast.success(`${t("state.deleted")}`)
}

View file

@ -3,6 +3,7 @@
v-if="show"
dialog
:title="t(`environment.${action}`)"
styles="sm:max-w-3xl"
@close="hideModal"
>
<template #body>
@ -91,15 +92,24 @@
:disabled="isViewer"
/>
<SmartEnvInput
v-model="env.value"
v-model="env.initialValue"
:placeholder="`${t('count.initialValue', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'initialValue' + index"
:secret="tab.isSecret"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
/>
<SmartEnvInput
v-model="env.currentValue"
:placeholder="`${t('count.currentValue', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
:name="'currentValue' + index"
:secret="tab.isSecret"
:readonly="isViewer && !tab.isSecret"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
/>
<div v-if="!isViewer" class="flex">
<HoppButtonSecondary
@ -166,14 +176,11 @@ import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { getEnvActionErrorMessage } from "~/helpers/error-messages"
import { CurrentValueService } from "~/services/current-environment-value.service"
type EnvironmentVariable = {
id: number
env: {
key: string
value: string
secret: boolean
}
env: Environment["variables"][number]
}
const t = useI18n()
@ -239,10 +246,14 @@ const tabsData: ComputedRef<
const editingName = ref<string | null>(null)
const editingID = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
{
id: idTicker.value++,
env: { key: "", currentValue: "", initialValue: "", secret: false },
},
])
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
const secretVars = computed(() =>
pipe(
@ -275,7 +286,9 @@ const evnExpandError = computed(() => {
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
A.exists(({ currentValue }) =>
E.isLeft(parseTemplateStringE(currentValue, variables))
)
)
})
@ -283,9 +296,28 @@ const liveEnvs = computed(() => {
if (evnExpandError.value) {
return []
}
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
return [
...vars.value.map((x) => ({ ...x.env, sourceEnv: editingName.value! })),
]
})
const getCurrentValue = (
editingID: string,
varIndex: number,
isSecret: boolean
) => {
if (isSecret) {
return secretEnvironmentService.getSecretEnvironmentVariable(
editingID,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
editingID,
varIndex
)?.currentValue
}
watch(
() => props.show,
(show) => {
@ -310,15 +342,13 @@ watch(
id: idTicker.value++,
env: {
key: e.key,
value: e.secret
? (secretEnvironmentService.getSecretEnvironmentVariable(
editingID.value ?? "",
index
)?.value ??
// @ts-expect-error `value` field can exist for secret environment variables as inferred while importing
e.value ??
"")
: e.value,
currentValue:
getCurrentValue(
props.editingEnvironment?.id ?? "",
index,
e.secret
) ?? "",
initialValue: e.initialValue,
secret: e.secret,
},
}))
@ -339,7 +369,8 @@ const addEnvironmentVariable = () => {
id: idTicker.value++,
env: {
key: "",
value: "",
currentValue: "",
initialValue: "",
secret: selectedEnvOption.value === "secret",
},
})
@ -377,17 +408,38 @@ const saveEnvironment = async () => {
const secretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
e.secret
? O.some({ key: e.key, value: e.currentValue, varIndex: i })
: O.none
)
)
const nonSecretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
!e.secret
? O.some({
key: e.key,
currentValue: e.currentValue,
varIndex: i,
isSecret: e.secret,
})
: O.none
)
)
const variables = pipe(
filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
A.map((e) => ({
key: e.key,
secret: e.secret,
initialValue: e.initialValue,
currentValue: "",
}))
)
const environmentUpdated: Environment = {
v: 1,
v: 2,
id: editingID.value ?? "",
name: editingName.value,
variables,
@ -419,6 +471,10 @@ const saveEnvironment = async () => {
envID,
secretVariables
)
currentEnvironmentValueService.addEnvironment(
envID,
nonSecretVariables
)
}
hideModal()
toast.success(`${t("environment.created")}`)
@ -463,7 +519,14 @@ const saveEnvironment = async () => {
() => {
hideModal()
toast.success(`${t("environment.updated")}`)
isLoading.value = false
if (editingID.value) {
currentEnvironmentValueService.addEnvironment(
editingID.value,
nonSecretVariables
)
}
}
)
)()

View file

@ -166,6 +166,7 @@ import IconEdit from "~icons/lucide/edit"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconSettings2 from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2"
import { CurrentValueService } from "~/services/current-environment-value.service"
const t = useI18n()
const toast = useToast()
@ -183,6 +184,7 @@ const emit = defineEmits<{
}>()
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
const confirmRemove = ref(false)
@ -214,6 +216,7 @@ const removeEnvironment = () => {
() => {
toast.success(`${t("team_environment.deleted")}`)
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
currentEnvironmentValueService.deleteEnvironment(props.environment.id)
}
)
)()

View file

@ -269,7 +269,6 @@ import linter from "~/helpers/editor/linting/rawKeyValue"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
import { commonHeaders } from "~/helpers/headers"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import {
ComputedHeader,
getComputedAuthHeaders,
@ -279,6 +278,7 @@ import {
AggregateEnvironment,
aggregateEnvs$,
getAggregateEnvs,
getCurrentEnvironment,
} from "~/newstore/environments"
import { toggleNestedSetting } from "~/newstore/settings"
import { InspectionService, InspectorResult } from "~/services/inspection"
@ -294,6 +294,8 @@ import IconPlus from "~icons/lucide/plus"
import IconTrash2 from "~icons/lucide/trash-2"
import IconWrapText from "~icons/lucide/wrap-text"
import { RESTOptionTabs } from "./RequestOptions.vue"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
const toast = useToast()
@ -311,6 +313,8 @@ const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpHeaders")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const currentEnvironmentValueService = useService(CurrentValueService)
// v-model integration with props and emit
const props = defineProps<{
modelValue:
@ -554,9 +558,25 @@ const computedHeaders: Ref<
}[]
> = ref([])
const currentSelectedEnvironment = getCurrentEnvironment()
watch([props.modelValue, aggregateEnvs], async () => {
const resolvedEnvs = aggregateEnvs.value.map((env) => {
return {
...env,
currentValue:
env.currentValue !== ""
? env.currentValue
: (currentEnvironmentValueService.getEnvironmentByKey(
env?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
env?.key ?? ""
)?.currentValue ?? ""),
}
})
computedHeaders.value = (
await getComputedHeaders(props.modelValue, aggregateEnvs.value, false)
await getComputedHeaders(props.modelValue, resolvedEnvs, true)
).map((header, index) => ({
id: `header-${index}`,
...header,

View file

@ -5,10 +5,10 @@
>
<input
v-if="isSecret"
id="secret"
:id="`secret-${uniqueID()}`"
v-model="secretText"
name="secret"
:placeholder="t('environment.secret_value')"
:placeholder="placeholder"
class="flex flex-1 bg-transparent pl-4"
:class="styles"
type="password"
@ -97,6 +97,7 @@ import { CompletionContext, autocompletion } from "@codemirror/autocomplete"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { syntaxTree } from "@codemirror/language"
import { uniqueID } from "~/helpers/utils/uniqueID"
const t = useI18n()
@ -374,11 +375,13 @@ const envVars = computed(() => {
if (props.envs) {
return props.envs.map((x) => {
const { key, secret } = x
const value = secret ? "********" : x.value
const currentValue = secret ? "********" : x.currentValue
const initialValue = secret ? "********" : x.initialValue
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
return {
key,
value,
currentValue,
initialValue,
sourceEnv,
secret,
}
@ -393,12 +396,14 @@ const envVars = computed(() => {
? tabs.currentActiveTab.value.document.request.requestVariables
: []
// Transform request variables to match the env format
return [
...requestVariables.map(({ active, key, value }) =>
active
? {
key,
value,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
}
@ -412,7 +417,7 @@ function envAutoCompletion(context: CompletionContext) {
const options = (envVars.value ?? [])
.map((env) => ({
label: env?.key ? `<<${env.key}>>` : "",
info: env?.value ?? "",
info: env?.currentValue ?? "",
apply: env?.key ? `<<${env.key}>>` : "",
}))
.filter(Boolean)
@ -539,6 +544,7 @@ const getExtensions = (readonly: boolean): Extension => {
override: [envAutoCompletion],
})
: [],
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {

View file

@ -44,8 +44,13 @@ import {
getTemporaryVariables,
setTemporaryVariables,
} from "./runner/temp_envs"
import {
CurrentValueService,
Variable,
} from "~/services/current-environment-value.service"
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
export const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
@ -99,11 +104,12 @@ export const executedResponses$ = new Subject<
* @param type Whether the environment variables are global or selected
* @returns the updated environment variables
*/
const updateEnvironmentsWithSecret = (
const updateEnvironments = (
envs: Environment["variables"] &
{
secret: true
value: string | undefined
currentValue: string
initialValue: string
key: string
}[],
type: "global" | "selected"
@ -112,6 +118,7 @@ const updateEnvironmentsWithSecret = (
type === "selected" ? getCurrentEnvironment().id : "Global"
const updatedSecretEnvironments: SecretVariable[] = []
const nonSecretVariables: Variable[] = []
const updatedEnv = pipe(
envs,
@ -119,16 +126,35 @@ const updateEnvironmentsWithSecret = (
if (e.secret) {
updatedSecretEnvironments.push({
key: e.key,
value: e.value ?? "",
value: e.currentValue ?? "",
varIndex: index,
})
// delete the value from the environment
// so that it doesn't get saved in the environment
delete e.value
return e
return {
key: e.key,
secret: e.secret,
initialValue: e.initialValue ?? "",
currentValue: "",
}
}
nonSecretVariables.push({
key: e.key,
isSecret: e.secret,
varIndex: index,
currentValue: e.currentValue ?? "",
})
// set the current value as empty string
// so that it doesn't get saved in the environment
return {
key: e.key,
secret: e.secret,
initialValue: e.initialValue ?? "",
currentValue: "",
}
return e
})
)
if (currentEnvID) {
@ -136,10 +162,39 @@ const updateEnvironmentsWithSecret = (
currentEnvID,
updatedSecretEnvironments
)
currentEnvironmentValueService.addEnvironment(
currentEnvID,
nonSecretVariables
)
}
return updatedEnv
}
/**
* Get the environment variable value from the current environment
* @param envID The environment ID
* @param index The index of the environment variable
* @param isSecret Whether the environment variable is a secret
* @returns The environment variable value
*/
const getEnvironmentVariableValue = (
envID: string,
index: number,
isSecret: boolean
): string | undefined => {
if (isSecret) {
return secretEnvironmentService.getSecretEnvironmentVariableValue(
envID,
index
)
}
return currentEnvironmentValueService.getEnvironmentVariableValue(
envID,
index
)
}
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
@ -149,7 +204,6 @@ const filterNonEmptyEnvironmentVariables = (
envs: Environment["variables"]
): Environment["variables"] => {
const envsMap = new Map<string, Environment["variables"][number]>()
envs.forEach((env) => {
if (env.secret) {
envsMap.set(env.key, env)
@ -158,9 +212,9 @@ const filterNonEmptyEnvironmentVariables = (
if (
existingEnv &&
"value" in existingEnv &&
existingEnv.value === "" &&
env.value !== ""
"currentValue" in existingEnv &&
existingEnv.currentValue === "" &&
env.currentValue !== ""
) {
envsMap.set(env.key, env)
}
@ -231,7 +285,8 @@ export function runRESTRequest$(
if (v.active) {
return {
key: v.key,
value: v.value,
initialValue: v.value,
currentValue: v.value,
secret: false,
}
}
@ -256,7 +311,7 @@ export function runRESTRequest$(
const effectiveRequest = await getEffectiveRESTRequest(finalRequest, {
id: "env-id",
v: 1,
v: 2,
name: "Env",
variables: finalEnvsWithNonEmptyValues,
})
@ -269,10 +324,7 @@ export function runRESTRequest$(
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") {
executedResponses$.next(
// @ts-expect-error Typescript can't figure out this inference for some reason
res
)
executedResponses$.next(res)
const runResult = await runTestScript(
res.req.testScript,
@ -287,10 +339,10 @@ export function runRESTRequest$(
if (E.isRight(runResult)) {
// set the response in the tab so that multiple tabs can run request simultaneously
tab.value.document.response = res
const updatedRunResult = updateEnvsAfterTestScript(runResult)
tab.value.document.testResults =
// @ts-expect-error Typescript can't figure out this inference for some reason
translateToSandboxTestResults(updatedRunResult)
tab.value.document.testResults = translateToSandboxTestResults(
runResult.right
)
updateEnvsAfterTestScript(runResult)
} else {
tab.value.document.testResults = {
description: "",
@ -323,13 +375,13 @@ export function runRESTRequest$(
}
function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
const updatedGlobalEnvVariables = updateEnvironments(
// @ts-expect-error Typescript can't figure out this inference for some reason
cloneDeep(runResult.right.envs.global),
"global"
)
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
const updatedSelectedEnvVariables = updateEnvironments(
// @ts-expect-error Typescript can't figure out this inference for some reason
cloneDeep(runResult.right.envs.selected),
"selected"
@ -343,14 +395,14 @@ function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
},
}
const globalEnvVariables = updateEnvironmentsWithSecret(
const globalEnvVariables = updateEnvironments(
// @ts-expect-error Typescript can't figure out this inference for some reason
runResult.right.envs.global,
"global"
)
setGlobalEnvVariables({
v: 1,
v: 2,
variables: globalEnvVariables,
})
if (environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV") {
@ -360,7 +412,7 @@ function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
})
updateEnvironment(environmentsStore.value.selectedEnvironmentIndex.index, {
name: env.name,
v: 1,
v: 2,
id: "id" in env ? env.id : "",
variables: updatedRunResult.envs.selected,
})
@ -411,7 +463,7 @@ export function runTestRunnerRequest(
const effectiveRequest = await getEffectiveRESTRequest(request, {
id: "env-id",
v: 1,
v: 2,
name: "Env",
variables: combineEnvVariables({
environments: {
@ -429,10 +481,7 @@ export function runTestRunnerRequest(
.toPromise()
.then(async (res) => {
if (res?.type === "success" || res?.type === "fail") {
executedResponses$.next(
// @ts-expect-error Typescript can't figure out this inference for some reason
res
)
executedResponses$.next(res)
const runResult = await runTestScript(
res.req.testScript,
@ -528,12 +577,12 @@ const getUpdatedEnvVariables = (
),
O.chain(
O.fromPredicate(
({ env, index }) => env.value !== current[index].value
({ env, index }) => env.currentValue !== current[index].currentValue
)
),
O.map(({ env, index }) => ({
...env,
previousValue: current[index].value,
previousValue: current[index].currentValue,
}))
)
)
@ -550,8 +599,21 @@ function translateToSandboxTestResults(
}
}
const globals = cloneDeep(getGlobalVariables())
const env = getCurrentEnvironment()
const globals = cloneDeep(getGlobalVariables()).map((g, index) => ({
...g,
currentValue: getEnvironmentVariableValue("Global", index, g.secret) ?? "",
}))
const envVars = getCurrentEnvironment().variables.map((e, index) => ({
...e,
currentValue:
getEnvironmentVariableValue(
getCurrentEnvironment().id,
index,
e.secret
) ?? "",
}))
return {
description: "",
expectResults: testDesc.tests.expectResults,
@ -564,15 +626,9 @@ function translateToSandboxTestResults(
updations: getUpdatedEnvVariables(globals, testDesc.envs.global),
},
selected: {
additions: getAddedEnvVariables(env.variables, testDesc.envs.selected),
deletions: getRemovedEnvVariables(
env.variables,
testDesc.envs.selected
),
updations: getUpdatedEnvVariables(
env.variables,
testDesc.envs.selected
),
additions: getAddedEnvVariables(envVars, testDesc.envs.selected),
deletions: getRemovedEnvVariables(envVars, testDesc.envs.selected),
updations: getUpdatedEnvVariables(envVars, testDesc.envs.selected),
},
},
}

View file

@ -26,6 +26,7 @@ import IconEdit from "~icons/lucide/edit?raw"
import IconUser from "~icons/lucide/user?raw"
import IconUsers from "~icons/lucide/users?raw"
import { isComment } from "./helpers"
import { CurrentValueService } from "~/services/current-environment-value.service"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@ -38,6 +39,8 @@ const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const restTabs = getService(RESTTabService)
/**
@ -54,7 +57,7 @@ const filterNonEmptyEnvironmentVariables = (
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (existingEnv?.value === "" && env.value !== "") {
if (existingEnv?.currentValue === "" && env.currentValue !== "") {
envsMap.set(env.key, env)
}
} else {
@ -110,11 +113,20 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
(env) => env.key === parsedEnvKey
)
const currentSelectedEnvironment = getCurrentEnvironment()
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
let envValue = "Not Found"
const currentSelectedEnvironment = getCurrentEnvironment()
const envTooltipValue =
tooltipEnv?.sourceEnv !== "RequestVariable"
? (currentEnvironmentValueService.getEnvironmentByKey(
tooltipEnv?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipEnv?.key ?? ""
)?.currentValue ?? "")
: tooltipEnv?.currentValue
const hasSecretEnv = secretEnvironmentService.hasSecretValue(
tooltipEnv?.sourceEnv !== "Global"
@ -123,14 +135,15 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
tooltipEnv?.key ?? ""
)
if (!tooltipEnv?.secret && tooltipEnv?.value) envValue = tooltipEnv.value
else if (tooltipEnv?.secret && hasSecretEnv) {
if (!tooltipEnv?.secret && envTooltipValue) {
envValue = envTooltipValue
} else if (tooltipEnv?.secret && hasSecretEnv) {
envValue = "******"
} else if (tooltipEnv?.secret && !hasSecretEnv) {
envValue = "Empty"
} else if (!tooltipEnv?.sourceEnv) {
envValue = "Not Found"
} else if (!tooltipEnv?.value) {
} else if (!envTooltipValue) {
envValue = "Empty"
}
@ -295,7 +308,8 @@ export class HoppEnvironmentPlugin {
this.envs = [
...requestVariables.map(({ key, value }) => ({
key,
value,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
})),
@ -318,7 +332,8 @@ export class HoppEnvironmentPlugin {
this.envs = [
...requestVariables.map(({ key, value }) => ({
key,
value,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
})),

View file

@ -19,7 +19,7 @@ const getEnvironmentJSON = (
? environmentIndex
: environmentObj.id
// Eliminate `value` field from secret environment variables prior to export
// Eliminate `currentValue` field from environment variables prior to export
const transformedEnvironment = transformEnvironmentVariables(newEnvironment)
return environmentId !== null
@ -39,17 +39,16 @@ export const transformEnvironmentVariables = ({
v,
name,
variables: variables.map((variable) => {
const { key, secret } = variable
const { key, secret, initialValue } = variable
// Eliminate `value` field for secret environment variables
if (secret) {
return {
key,
secret,
}
// Eliminate `currentValue` field for secret environment variables and currentValue
return {
key,
secret,
initialValue,
currentValue: "",
}
return variable
}),
}
}

View file

@ -24,8 +24,8 @@ export const hoppEnvImporter = (contents: string[]) => {
...contentEntry,
variables: contentEntry.variables?.map((valueEntry) => ({
...valueEntry,
...("value" in valueEntry
? { value: String(valueEntry.value) }
...("initialValue" in valueEntry
? { value: String(valueEntry.initialValue) }
: {}),
})),
}

View file

@ -64,14 +64,18 @@ export const insomniaEnvImporter = (contents: string[]) => {
insomniaEnvs.forEach((insomniaEnv) => {
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
if (parsedInsomniaEnv.success) {
const environment: NonSecretEnvironment = {
id: uniqueID(),
v: 1,
v: 2,
name: parsedInsomniaEnv.data.name,
variables: Object.entries(parsedInsomniaEnv.data.data).map(
([key, value]) => ({ key, value, secret: false })
([key, value]) => ({
key,
initialValue: value,
currentValue: value,
secret: false,
})
),
}
@ -83,7 +87,8 @@ export const insomniaEnvImporter = (contents: string[]) => {
...env,
variables: env.variables.map((variable) => ({
...variable,
value: replaceInsomniaTemplating(variable.value),
initialValue: replaceInsomniaTemplating(variable.initialValue),
currentValue: replaceInsomniaTemplating(variable.currentValue),
})),
}))

View file

@ -52,11 +52,12 @@ export const postmanEnvImporter = (contents: string[]) => {
const environments: Environment[] = validationResult.data.map(
({ name, values }) => ({
id: uniqueID(),
v: 1,
v: 2,
name,
variables: values.map(({ key, value, type }) => ({
key,
value,
initialValue: value,
currentValue: value,
secret: type === "secret",
})),
})

View file

@ -10,10 +10,18 @@ import {
import { TestResult } from "@hoppscotch/js-sandbox"
import { getService } from "~/modules/dioc"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const unsecretEnvironments = (
/**
* Populate the currentValue of the environment variables and set the secret values
* @param selected
* @param global
* @returns
*/
const unWrapEnvironments = (
selected: Environment,
global: Environment["variables"]
) => {
@ -22,18 +30,18 @@ const unsecretEnvironments = (
"Global",
index
)
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
"Global",
index
)
if (secretVar) {
return {
...globalVar,
value: secretVar.value,
}
} else if (!("value" in globalVar) || !globalVar.value) {
return {
...globalVar,
value: "",
currentValue: secretVar.value,
}
}
return globalVar
return { ...globalVar, currentValue: currentVar?.currentValue ?? "" }
})
const resolvedSelectedWithSecrets = selected.variables.map(
@ -42,18 +50,17 @@ const unsecretEnvironments = (
selected.id,
index
)
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
selected.id,
index
)
if (secretVar) {
return {
...selectedVar,
value: secretVar.value,
}
} else if (!("value" in selectedVar) || !selectedVar.value) {
return {
...selectedVar,
value: "",
currentValue: secretVar.value,
}
}
return selectedVar
return { ...selectedVar, currentValue: currentVar?.currentValue ?? "" }
}
)
@ -64,7 +71,7 @@ const unsecretEnvironments = (
}
export const getCombinedEnvVariables = (temp?: Environment["variables"]) => {
const reformedVars = unsecretEnvironments(
const reformedVars = unWrapEnvironments(
getCurrentEnvironment(),
getGlobalVariables()
)

View file

@ -118,7 +118,7 @@ export default class TeamEnvironmentAdapter {
id: x.id,
teamID: x.teamID,
environment: {
v: 1,
v: 2,
id: x.id,
name: x.name,
variables: JSON.parse(x.variables),
@ -198,7 +198,7 @@ export default class TeamEnvironmentAdapter {
id: x.id,
teamID: x.teamID,
environment: {
v: 1,
v: 2,
id: x.id,
name: x.name,
variables: JSON.parse(x.variables),
@ -253,7 +253,7 @@ export default class TeamEnvironmentAdapter {
id: x.id,
teamID: x.teamID,
environment: {
v: 1,
v: 2,
id: x.id,
name: x.name,
variables: JSON.parse(x.variables),

View file

@ -25,14 +25,14 @@ export type SelectedEnvironmentIndex =
}
const defaultGlobalEnvironmentState: GlobalEnvironment = {
v: 1,
v: 2,
variables: [],
}
const defaultEnvironmentsState = {
environments: [
{
v: 1,
v: 2,
id: uniqueID(),
name: "My Environment Variables",
variables: [],
@ -105,12 +105,12 @@ const dispatchers = defineDispatchers({
envID
? {
id: envID,
v: 1,
v: 2,
name,
variables,
}
: {
v: 1,
v: 2,
id: uniqueID(),
name,
variables,
@ -205,16 +205,26 @@ const dispatchers = defineDispatchers({
{
envIndex,
key,
value,
initialValue,
currentValue,
secret,
}: { envIndex: number; key: string; value: string; secret: boolean }
}: {
envIndex: number
key: string
initialValue: string
currentValue: string
secret: boolean
}
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
variables: [...env.variables, { key, value, secret }],
variables: [
...env.variables,
{ key, initialValue, currentValue, secret },
],
}
: env
),
@ -244,7 +254,12 @@ const dispatchers = defineDispatchers({
vars,
}: {
envIndex: number
vars: { key: string; value: string; secret: boolean }[]
vars: {
key: string
initialValue: string
currentValue: string
secret: boolean
}[]
}
) {
return {
@ -264,12 +279,14 @@ const dispatchers = defineDispatchers({
envIndex,
variableIndex,
updatedKey,
updatedValue,
updatedInitialValue,
updatedCurrentValue,
}: {
envIndex: number
variableIndex: number
updatedKey: string
updatedValue: string
updatedInitialValue: string
updatedCurrentValue: string
}
) {
return {
@ -279,7 +296,12 @@ const dispatchers = defineDispatchers({
...env,
variables: env.variables.map((v, vIndex) =>
vIndex === variableIndex
? { key: updatedKey, value: updatedValue, secret: v.secret }
? {
key: updatedKey,
initialValue: updatedInitialValue,
currentValue: updatedCurrentValue,
secret: v.secret,
}
: v
),
}
@ -380,7 +402,7 @@ export const currentEnvironment$: Observable<Environment | undefined> =
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
const env: Environment = {
name: "No environment",
v: 1,
v: 2,
id: "",
variables: [],
}
@ -395,7 +417,8 @@ export const currentEnvironment$: Observable<Environment | undefined> =
export type AggregateEnvironment = {
key: string
value: string
initialValue: string
currentValue: string
secret: boolean
sourceEnv: string
}
@ -415,7 +438,8 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => {
effectiveAggregateEnvs.push({
key,
value: getValue(),
currentValue: getValue(),
initialValue: getValue(),
secret: false,
sourceEnv: selectedEnv?.name ?? "Global",
})
@ -425,12 +449,16 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
const currentValue =
"currentValue" in variable ? variable.currentValue : ""
const initialValue =
"initialValue" in variable ? variable.initialValue : ""
if (!aggregateEnvKeys.includes(key)) {
effectiveAggregateEnvs.push({
key,
value,
currentValue,
initialValue,
secret,
sourceEnv: selectedEnv.name,
})
@ -439,10 +467,19 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
globalEnv.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
const currentValue =
"currentValue" in variable ? variable.currentValue : ""
const initialValue =
"initialValue" in variable ? variable.initialValue : ""
if (!aggregateEnvKeys.includes(key)) {
effectiveAggregateEnvs.push({ key, value, secret, sourceEnv: "Global" })
effectiveAggregateEnvs.push({
key,
currentValue,
initialValue,
secret,
sourceEnv: "Global",
})
}
})
@ -455,26 +492,28 @@ export function getAggregateEnvs() {
const currentEnv = getCurrentEnvironment()
return [
...currentEnv.variables.map((x) => {
let value
let currentValue
if (!x.secret) {
value = x.value
currentValue = x.currentValue
}
return <AggregateEnvironment>{
key: x.key,
value,
initialValue: x.initialValue,
currentValue,
secret: x.secret,
sourceEnv: currentEnv.name,
}
}),
...getGlobalVariables().map((x) => {
let value
let currentValue
if (!x.secret) {
value = x.value
currentValue = x.currentValue
}
return <AggregateEnvironment>{
key: x.key,
value,
initialValue: x.initialValue,
currentValue,
secret: x.secret,
sourceEnv: "Global",
}
@ -486,36 +525,40 @@ export function getAggregateEnvsWithSecrets() {
const currentEnv = getCurrentEnvironment()
return [
...currentEnv.variables.map((x, index) => {
let value
let currentValue
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
currentEnv.id,
index
)
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
currentEnv.id,
index
)
} else {
value = x.value
currentValue = x.currentValue
}
return <AggregateEnvironment>{
key: x.key,
value,
currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: currentEnv.name,
}
}),
...getGlobalVariables().map((x, index) => {
let value
let currentValue
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
)
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
)
} else {
value = x.value
currentValue = x.currentValue
}
return <AggregateEnvironment>{
key: x.key,
value,
currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: "Global",
}
@ -528,36 +571,40 @@ export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
map(([selectedEnv, globalEnv]) => {
const results: AggregateEnvironment[] = []
selectedEnv?.variables.map((x, index) => {
let value
let currentValue
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
selectedEnv.id,
index
)
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
selectedEnv.id,
index
)
} else {
value = x.value
currentValue = x.currentValue
}
results.push({
key: x.key,
value: value ?? "",
currentValue: currentValue ?? "",
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: selectedEnv.name,
})
})
globalEnv.variables.map((x, index) => {
let value
let currentValue
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
)
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
)
} else {
value = x.value
currentValue = x.currentValue
}
results.push({
key: x.key,
value: value ?? "",
currentValue: currentValue ?? "",
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: "Global",
})
@ -573,7 +620,7 @@ export function getCurrentEnvironment(): Environment {
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
) {
return {
v: 1,
v: 2,
id: "",
name: "No environment",
variables: [],
@ -620,7 +667,7 @@ export function getLegacyGlobalEnvironment(): Environment | null {
export function getGlobalVariables(): GlobalEnvironmentVariable[] {
return environmentsStore.value.globals.variables.map(
(env: GlobalEnvironmentVariable) => {
if (env.key && "value" in env && !("secret" in env)) {
if (env.key && "currentValue" in env && !("secret" in env)) {
return {
...(env as GlobalEnvironmentVariable),
secret: false,
@ -765,7 +812,12 @@ export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
export function setEnvironmentVariables(
envIndex: number,
vars: { key: string; value: string; secret: boolean }[]
vars: {
key: string
currentValue: string
initialValue: string
secret: boolean
}[]
) {
environmentsStore.dispatch({
dispatcher: "setEnvironmentVariables",
@ -778,14 +830,25 @@ export function setEnvironmentVariables(
export function addEnvironmentVariable(
envIndex: number,
{ key, value, secret }: { key: string; value: string; secret: boolean }
{
key,
currentValue,
initialValue,
secret,
}: {
key: string
currentValue: string
initialValue: string
secret: boolean
}
) {
environmentsStore.dispatch({
dispatcher: "addEnvironmentVariable",
payload: {
envIndex,
key,
value,
currentValue,
initialValue,
secret,
},
})
@ -807,7 +870,11 @@ export function removeEnvironmentVariable(
export function updateEnvironmentVariable(
envIndex: number,
variableIndex: number,
{ key, value }: { key: string; value: string }
{
key,
currentValue,
initialValue,
}: { key: string; currentValue: string; initialValue: string }
) {
environmentsStore.dispatch({
dispatcher: "updateEnvironmentVariable",
@ -815,7 +882,8 @@ export function updateEnvironmentVariable(
envIndex,
variableIndex,
updatedKey: key,
updatedValue: value,
updatedCurrentValue: currentValue,
updatedInitialValue: initialValue,
},
})
}

View file

@ -0,0 +1,178 @@
import { describe, it, expect, beforeEach } from "vitest"
import { TestContainer } from "dioc/testing"
import {
CurrentValueService,
Variable,
} from "../current-environment-value.service"
describe("CurrentValueService", () => {
let container: TestContainer
let service: CurrentValueService
beforeEach(() => {
container = new TestContainer()
service = container.bind(CurrentValueService)
})
describe("addEnvironment", () => {
it("should add a new environment with the provided ID and variables", () => {
const id = "env1"
const vars: Variable[] = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.addEnvironment(id, vars)
expect(service.environments.get(id)).toEqual(vars)
})
})
describe("getEnvironment", () => {
it("should return the variables of the specified environment", () => {
const id = "env1"
const vars: Variable[] = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
expect(service.getEnvironment(id)).toEqual(vars)
})
it("should return undefined for non-existent environment", () => {
expect(service.getEnvironment("nonexistent")).toBeUndefined()
})
})
describe("getEnvironmentVariable", () => {
it("should return the variable at the given index", () => {
const id = "env1"
const vars: Variable[] = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
expect(service.getEnvironmentVariable(id, 1)).toEqual(vars[0])
})
it("should return undefined for non-existent variable", () => {
const id = "env1"
const vars: Variable[] = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
expect(service.getEnvironmentVariable(id, 2)).toBeUndefined()
})
})
describe("getEnvironmentVariableValue", () => {
it("should return the value of the variable", () => {
const id = "env1"
const vars: Variable[] = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
expect(service.getEnvironmentVariableValue(id, 1)).toBe("value1")
})
it("should return undefined if variable doesn't exist", () => {
expect(service.getEnvironmentVariableValue("env1", 999)).toBeUndefined()
})
})
describe("loadEnvironmentsFromPersistedState", () => {
it("should load environments correctly", () => {
const state = {
env1: [{ key: "k", currentValue: "v", varIndex: 0, isSecret: false }],
}
service.loadEnvironmentsFromPersistedState(state)
expect(service.environments.get("env1")).toEqual(state.env1)
})
})
describe("deleteEnvironment", () => {
it("should delete the specified environment", () => {
const id = "env1"
service.environments.set(id, [])
service.deleteEnvironment(id)
expect(service.environments.has(id)).toBe(false)
})
})
describe("removeEnvironmentVariable", () => {
it("should remove the specified variable", () => {
const id = "env1"
const vars = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
{ key: "key2", currentValue: "value2", varIndex: 2, isSecret: false },
]
service.environments.set(id, vars)
service.removeEnvironmentVariable(id, 1)
expect(service.environments.get(id)).toEqual([vars[1]])
})
it("should not fail if variable does not exist", () => {
const id = "env1"
const vars = [
{ key: "key1", currentValue: "value1", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
service.removeEnvironmentVariable(id, 999)
expect(service.environments.get(id)).toEqual(vars)
})
})
describe("updateEnvironmentID", () => {
it("should update the environment ID", () => {
const oldID = "old"
const newID = "new"
const vars = [
{ key: "k", currentValue: "v", varIndex: 0, isSecret: false },
]
service.environments.set(oldID, vars)
service.updateEnvironmentID(oldID, newID)
expect(service.environments.has(oldID)).toBe(false)
expect(service.environments.get(newID)).toEqual(vars)
})
})
describe("hasValue", () => {
it("should return true if a variable with the key and non-empty value exists", () => {
const id = "env1"
const vars = [
{ key: "k", currentValue: "v", varIndex: 0, isSecret: true },
]
service.environments.set(id, vars)
expect(service.hasValue(id, "k")).toBe(true)
})
it("should return false if no variable with key exists or value is empty", () => {
service.environments.set("env1", [
{ key: "k", currentValue: "", varIndex: 0, isSecret: true },
])
expect(service.hasValue("env1", "k")).toBe(false)
})
})
describe("getEnvironmentByKey", () => {
it("should return variable by key", () => {
const id = "env1"
const vars = [
{ key: "k", currentValue: "v", varIndex: 1, isSecret: false },
]
service.environments.set(id, vars)
expect(service.getEnvironmentByKey(id, "k")).toEqual(vars[0])
})
it("should return undefined if key not found", () => {
service.environments.set("env1", [])
expect(service.getEnvironmentByKey("env1", "k")).toBeUndefined()
})
})
describe("persistableEnvironments", () => {
it("should return the environments in persistable format", () => {
const id = "env1"
const vars = [
{ key: "k", currentValue: "v", varIndex: 0, isSecret: false },
]
service.environments.set(id, vars)
expect(service.persistableEnvironments.value).toEqual({ [id]: vars })
})
})
})

View file

@ -0,0 +1,162 @@
import { Service } from "dioc"
import { cloneDeep } from "lodash-es"
import { reactive, computed } from "vue"
/**
* Defines a environment variable.
*/
export type Variable = {
key: string
currentValue: string
varIndex: number
isSecret: boolean
}
/**
* This service is used to store and manage current value of environment variables.
* The current value are not synced with the server.
* hence they are not persisted in the database. They are stored
* in the local storage of the browser.
*/
export class CurrentValueService extends Service {
public static readonly ID = "CURRENT_VALUE_SERVICE"
/**
* Map of current value of environments.
* The key is the ID of the environment.
* The value is the list of environment variables.
*/
public environments = reactive(new Map<string, Variable[]>())
/**
* Add a new environment.
* @param id ID of the environment
* @param vars List of environment variables
*/
public addEnvironment(id: string, vars: Variable[]) {
this.environments.set(id, vars)
}
/**
* Get a environment.
* @param id ID of the environment
*/
public getEnvironment(id: string) {
return this.environments.get(id)
}
/**
* Add a new environment variable to the environment.
* If the environment does not exist, it will be created.
* @param id ID of the environment
* @param variable Environment variable to add
*/
public addEnvironmentVariable(id: string, variable: Variable) {
const vars = this.getEnvironment(id)
if (vars) {
const newVars = cloneDeep(vars)
newVars.push(variable)
this.environments.set(id, newVars)
} else {
this.environments.set(id, [variable])
}
}
/**
* Get a environment variable.
* @param id ID of the environment
* @param varIndex Index of the variable in the environment
*/
public getEnvironmentVariable(id: string, varIndex: number) {
const vars = this.getEnvironment(id)
return vars?.find((v) => v.varIndex === varIndex)
}
/**
* Used to get the value of a environment variable.
* @param id ID of the environment
* @param varIndex Index of the variable in the environment
*/
public getEnvironmentVariableValue(id: string, varIndex: number) {
const variable = this.getEnvironmentVariable(id, varIndex)
return variable?.currentValue
}
/**
*
* @param environments Used to load environments from persisted state.
*/
public loadEnvironmentsFromPersistedState(
environments: Record<string, Variable[]>
) {
if (environments) {
this.environments.clear()
Object.entries(environments).forEach(([id, vars]) => {
this.addEnvironment(id, vars)
})
}
}
/**
* Delete a environment.
* @param id ID of the environment
*/
public deleteEnvironment(id: string) {
this.environments.delete(id)
}
/**
* Delete a environment variable.
* @param id ID of the environment
* @param varIndex Index of the variable in the environment
*/
public removeEnvironmentVariable(id: string, varIndex: number) {
const vars = this.getEnvironment(id)
const newVars = vars?.filter((v) => v.varIndex !== varIndex)
this.environments.set(id, newVars || [])
}
/**
* Used to update the ID of a environment.
* Used while syncing with the server.
* @param oldID old ID of the environment
* @param newID new ID of the environment
*/
public updateEnvironmentID(oldID: string, newID: string) {
const vars = this.getEnvironment(oldID)
this.environments.set(newID, vars || [])
this.environments.delete(oldID)
}
/**
*
* @param id ID of the environment
* @param key Key of the variable to check the value exists
* @returns true if the key has a secret value
*/
public hasValue(id: string, key: string) {
return (
this.environments.has(id) &&
this.environments
.get(id)!
.some((v) => v.key === key && v.currentValue !== "")
)
}
public getEnvironmentByKey(id: string, key: string) {
const vars = this.getEnvironment(id)
return vars?.find((v) => v.key === key)
}
/**
* Used to update the value of a environment variable.
*/
public persistableEnvironments = computed(() => {
const environments: Record<string, Variable[]> = {}
this.environments.forEach((vars, id) => {
environments[id] = vars
})
return environments
})
}

View file

@ -4,6 +4,7 @@ import { EnvironmentInspectorService } from "../environment.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { CurrentValueService } from "~/services/current-environment-value.service"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
@ -16,8 +17,18 @@ vi.mock("~/newstore/environments", async () => {
return {
__esModule: true,
aggregateEnvsWithSecrets$: new BehaviorSubject([
{ key: "EXISTING_ENV_VAR", value: "test_value", secret: false },
{ key: "EXISTING_ENV_VAR_2", value: "", secret: false },
{
key: "EXISTING_ENV_VAR",
currentValue: "test_value",
initialValue: "test_value",
secret: false,
},
{
key: "EXISTING_ENV_VAR_2",
currentValue: "",
initialValue: "",
secret: false,
},
]),
getCurrentEnvironment: () => ({
id: "1",
@ -25,7 +36,8 @@ vi.mock("~/newstore/environments", async () => {
v: 1,
variables: {
key: "EXISTING_ENV_VAR",
value: "test_value",
currentValue: "test_value",
initialValue: "test_value",
secret: false,
},
}),
@ -75,6 +87,12 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the URL contains defined environment variables", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR_2") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
@ -95,7 +113,12 @@ describe("EnvironmentInspectorService", () => {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<UNDEFINED_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -115,13 +138,24 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the headers contain defined environment variables", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR_2") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -138,7 +172,12 @@ describe("EnvironmentInspectorService", () => {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
params: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<UNDEFINED_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -158,6 +197,12 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the params contain defined environment variables", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
@ -165,7 +210,12 @@ describe("EnvironmentInspectorService", () => {
endpoint: "http://example.com/api/data",
headers: [],
params: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -176,6 +226,12 @@ describe("EnvironmentInspectorService", () => {
it("should return an inspector result when the URL contains empty value in a environment variable", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR_2") return true
return false
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
@ -190,6 +246,12 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the URL contains non empty value in a environment variable", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
@ -210,7 +272,12 @@ describe("EnvironmentInspectorService", () => {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<EXISTING_ENV_VAR_2>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR_2>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -221,13 +288,24 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the headers contain non empty value in a environment variable", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -245,7 +323,12 @@ describe("EnvironmentInspectorService", () => {
endpoint: "http://example.com/api/data",
headers: [],
params: [
{ key: "<<EXISTING_ENV_VAR_2>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR_2>>",
value: "some-value",
active: true,
description: "",
},
],
})
@ -256,6 +339,12 @@ describe("EnvironmentInspectorService", () => {
it("should not return an inspector result when the params contain non empty value in a environment variable", () => {
const container = new TestContainer()
container.bindMock(CurrentValueService, {
hasValue: vi.fn((key) => {
if (key === "EXISTING_ENV_VAR") return false
return true
}),
})
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
@ -263,7 +352,12 @@ describe("EnvironmentInspectorService", () => {
endpoint: "http://example.com/api/data",
headers: [],
params: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
{
key: "<<EXISTING_ENV_VAR>>",
value: "some-value",
active: true,
description: "",
},
],
})

View file

@ -23,6 +23,7 @@ import { computed } from "vue"
import { useStreamStatic } from "~/composables/stream"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
import { CurrentValueService } from "~/services/current-environment-value.service"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@ -46,6 +47,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService)
private readonly secretEnvs = this.bind(SecretEnvironmentService)
private readonly currentEnvs = this.bind(CurrentValueService)
private readonly restTabs = this.bind(RESTTabService)
private aggregateEnvsWithSecrets = useStreamStatic(
@ -157,7 +159,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (existingEnv?.value === "" && env.value !== "") {
if (existingEnv?.currentValue === "" && env.currentValue !== "") {
envsMap.set(env.key, env)
}
} else {
@ -200,8 +202,11 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const environmentVariables =
this.filterNonEmptyEnvironmentVariables([
// Transform the request variables to environment variables
...(currentTabRequest?.requestVariables ?? []).map((env) => ({
...env,
key: env.key,
currentValue: env.value,
initialValue: env.value,
secret: false,
sourceEnv: "RequestVariable",
})),
@ -216,8 +221,15 @@ export class EnvironmentInspectorService extends Service implements Inspector {
env.key
)
const hasCurrentValue = this.currentEnvs.hasValue(
env.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
env.key
)
if (env.key === formattedExEnv) {
if (env.secret ? !hasSecretEnv : env.value === "") {
if (env.secret ? !hasSecretEnv : !hasCurrentValue) {
const itemLocation: InspectorLocation = {
type: locations.type,
position:

View file

@ -81,22 +81,30 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
export const ENVIRONMENTS_MOCK: Environment[] = [
{
v: 1,
v: 2,
id: "ENV_1",
name: "globals",
variables: [
{
key: "test-global-key",
value: "test-global-value",
initialValue: "test-global-value",
currentValue: "test-global-value",
secret: false,
},
],
},
{
v: 1,
v: 2,
id: "ENV_2",
name: "Test",
variables: [{ key: "test-key", value: "test-value", secret: false }],
variables: [
{
key: "test-key",
initialValue: "test-value",
currentValue: "test-value",
secret: false,
},
],
},
]
@ -127,8 +135,15 @@ export const MQTT_REQUEST_MOCK = {
}
export const GLOBAL_ENV_MOCK: GlobalEnvironment = {
v: 1,
variables: [{ key: "test-key", value: "test-value", secret: false }],
v: 2,
variables: [
{
key: "test-key",
currentValue: "test-value",
initialValue: "test-value",
secret: false,
},
],
}
export const VUEX_DATA_MOCK: VUEX_DATA = {

View file

@ -69,6 +69,7 @@ import { SIORequest$, setSIORequest } from "../../newstore/SocketIOSession"
import { WSRequest$, setWSRequest } from "../../newstore/WebSocketSession"
import {
CURRENT_ENVIRONMENT_VALUE_SCHEMA,
ENVIRONMENTS_SCHEMA,
GLOBAL_ENVIRONMENT_SCHEMA,
GQL_COLLECTION_SCHEMA,
@ -92,6 +93,10 @@ import {
import { PersistableTabState } from "../tab"
import { HoppTabDocument } from "~/helpers/rest/document"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import {
CurrentValueService,
Variable,
} from "../current-environment-value.service"
export const STORE_NAMESPACE = "persistence.v1"
@ -113,6 +118,7 @@ export const STORE_KEYS = {
REST_TABS: "restTabs",
GQL_TABS: "gqlTabs",
SECRET_ENVIRONMENTS: "secretEnvironments",
CURRENT_ENVIRONMENT_VALUE: "currentEnvironmentValue",
SCHEMA_VERSION: "schema_version",
} as const
@ -175,6 +181,8 @@ export class PersistenceService extends Service {
private readonly secretEnvironmentService = this.bind(
SecretEnvironmentService
)
private readonly currentEnvironmentValueService =
this.bind(CurrentValueService)
private showErrorToast(key: string) {
const toast = useToast()
@ -600,6 +608,55 @@ export class PersistenceService extends Service {
)
}
private async setupCurrentEnvironmentValuePersistence() {
const loadResult = await Store.get<any>(
STORE_NAMESPACE,
STORE_KEYS.CURRENT_ENVIRONMENT_VALUE
)
try {
if (E.isRight(loadResult) && loadResult.right) {
const result = CURRENT_ENVIRONMENT_VALUE_SCHEMA.safeParse(
loadResult.right
)
if (result.success) {
this.currentEnvironmentValueService.loadEnvironmentsFromPersistedState(
result.data
)
} else {
this.showErrorToast(STORE_KEYS.CURRENT_ENVIRONMENT_VALUE)
await Store.set(
STORE_NAMESPACE,
`${STORE_KEYS.CURRENT_ENVIRONMENT_VALUE}-backup`,
loadResult.right
)
console.error(
`Failed parsing persisted CURRENT_ENVIRONMENT_VALUE:`,
JSON.stringify(loadResult.right)
)
}
}
} catch (e) {
console.error(
`Failed parsing persisted CURRENT_ENVIRONMENT_VALUE:`,
loadResult
)
}
watchDebounced(
this.currentEnvironmentValueService.persistableEnvironments,
async (newData: Record<string, Variable[]>) => {
await Store.set(
STORE_NAMESPACE,
STORE_KEYS.CURRENT_ENVIRONMENT_VALUE,
newData
)
},
{ debounce: 500 }
)
}
private async setupSelectedEnvPersistence() {
const loadResult = await Store.get<any>(
STORE_NAMESPACE,
@ -914,6 +971,7 @@ export class PersistenceService extends Service {
this.setupGQLTabsPersistence(),
this.setupSecretEnvironmentsPersistence(),
this.setupCurrentEnvironmentValuePersistence(),
])
}

View file

@ -228,12 +228,9 @@ export const MQTT_REQUEST_SCHEMA = z.nullable(
const EnvironmentVariablesSchema = z.union([
z.object({
key: z.string(),
value: z.string(),
secret: z.literal(false).catch(false),
}),
z.object({
key: z.string(),
secret: z.literal(true),
initialValue: z.string(),
currentValue: z.string(),
secret: z.boolean(),
}),
z.object({
key: z.string(),
@ -376,6 +373,24 @@ export const SECRET_ENVIRONMENT_VARIABLE_SCHEMA = z.union([
),
])
export const CURRENT_ENVIRONMENT_VALUE_SCHEMA = z.union([
z.object({}).strict(),
z.record(
z.string(),
z.array(
z
.object({
key: z.string(),
currentValue: z.string(),
varIndex: z.number(),
isSecret: z.boolean(),
})
.strict()
)
),
])
const HoppTestResultSchema = z
.object({
tests: z.array(HoppTestDataSchema),

View file

@ -57,7 +57,7 @@ export class SecretEnvironmentService extends Service {
* Used to get the value of a secret environment variable.
* @param id ID of the environment
* @param varIndex Index of the variable in the environment
= */
*/
public getSecretEnvironmentVariableValue(id: string, varIndex: number) {
const secretVar = this.getSecretEnvironmentVariable(id, varIndex)
return secretVar?.value
@ -101,7 +101,7 @@ export class SecretEnvironmentService extends Service {
}
/**
* Used to update thye ID of a secret environment.
* Used to update the ID of a secret environment.
* Used while syncing with the server.
* @param oldID old ID of the environment
* @param newID new ID of the environment

View file

@ -82,6 +82,10 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
public readonly searcherID = "environments"
public searcherSectionTitle = this.t("spotlight.environments.title")
private readonly workspaceService = this.bind(WorkspaceService)
private workspace = this.workspaceService.currentWorkspace
private readonly spotlight = this.bind(SpotlightService)
private selectedEnvIndex = useStreamStatic(
@ -239,10 +243,17 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
})
break
case "edit_selected_env":
if (this.selectedEnv.value)
invokeAction(`modals.my.environment.edit`, {
envName: this.selectedEnv.value.name,
})
if (this.selectedEnv.value) {
if (this.workspace.value.type === "personal") {
invokeAction(`modals.my.environment.edit`, {
envName: this.selectedEnv.value.name,
})
} else {
invokeAction(`modals.team.environment.edit`, {
envName: this.selectedEnv.value.name,
})
}
}
break
case "delete_selected_env":
invokeAction(`modals.environment.delete-selected`)

View file

@ -6,6 +6,7 @@ import { z } from "zod"
import V0_VERSION from "./v/0"
import V1_VERSION, { uniqueID } from "./v/1"
import V2_VERSION from "./v/2"
import { HOPP_SUPPORTED_PREDEFINED_VARIABLES } from "../predefinedVariables"
const versionedObject = z.object({
@ -13,10 +14,11 @@ const versionedObject = z.object({
})
export const Environment = createVersionedEntity({
latestVersion: 1,
latestVersion: 2,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
@ -48,7 +50,7 @@ const ENV_MAX_EXPAND_LIMIT = 10
*/
const ENV_EXPAND_LOOP = "ENV_EXPAND_LOOP" as const
export const EnvironmentSchemaVersion = 1
export const EnvironmentSchemaVersion = 2
export function parseBodyEnvVariablesE(
body: string,
@ -72,8 +74,8 @@ export function parseBodyEnvVariablesE(
const foundEnv = env.find((envVar) => envVar.key === variableName)
if (foundEnv && "value" in foundEnv) {
return foundEnv.value
if (foundEnv && "currentValue" in foundEnv) {
return foundEnv.currentValue
}
return key
})
@ -102,7 +104,11 @@ export function parseTemplateStringE(
str: string,
variables:
| Environment["variables"]
| { secret: true; value: string; key: string }[],
| {
secret: true
currentValue: string
key: string
}[],
maskValue = false,
showKeyIfSecret = false
) {
@ -131,7 +137,7 @@ export function parseTemplateStringE(
const variable = variables.find((x) => x && x.key === p1)
if (variable && "value" in variable) {
if (variable && "currentValue" in variable) {
// Show the key if it is a secret and explicitly specified
if (variable.secret && showKeyIfSecret) {
isSecret = true
@ -140,11 +146,17 @@ export function parseTemplateStringE(
// Mask the value if it is a secret and explicitly specified
if (variable.secret && maskValue) {
return "*".repeat(
(variable as { secret: true; value: string; key: string }).value
.length
(
variable as {
secret: true
initialValue: string
currentValue: string
key: string
}
).currentValue.length
)
}
return variable.value
return variable.currentValue
}
return ""
@ -173,7 +185,11 @@ export const parseTemplateString = (
str: string,
variables:
| Environment["variables"]
| { secret: true; value: string; key: string }[],
| {
secret: true
currentValue: string
key: string
}[],
maskValue = false,
showKeyIfSecret = false
) =>
@ -187,7 +203,8 @@ export const translateToNewEnvironmentVariables = (
): Environment["variables"][number] => {
return {
key: x.key,
value: x.value,
initialValue: x.initialValue ?? x.value ?? "",
currentValue: x.currentValue ?? x.value ?? "",
secret: false,
}
}

View file

@ -0,0 +1,40 @@
import { boolean, z } from "zod"
import { defineVersion } from "verzod"
import { V1_SCHEMA } from "./1"
// add initialValue and currentValue to the schema and delete value and add it to initialValue and currentValue
export const V2_SCHEMA = V1_SCHEMA.extend({
v: z.literal(2),
variables: z.array(
z.object({
key: z.string(),
initialValue: z.string(),
currentValue: z.string(),
secret: z.boolean(),
})
),
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
const result: z.infer<typeof V2_SCHEMA> = {
...old,
v: 2,
variables: old.variables.map((variable) => {
// if the variable is secret, set initialValue and currentValue to empty string
// else set initialValue and currentValue to value
// and delete value
return {
...variable,
initialValue: variable.secret ? "" : variable.value,
currentValue: variable.secret ? "" : variable.value,
value: undefined,
}
}),
}
return result
},
})

View file

@ -4,16 +4,18 @@ import { z } from "zod"
import V0_VERSION from "./v/0"
import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
const versionedObject = z.object({
v: z.number(),
})
export const GlobalEnvironment = createVersionedEntity({
latestVersion: 1,
latestVersion: 2,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)

View file

@ -0,0 +1,39 @@
import { z } from "zod"
import { defineVersion } from "verzod"
import { V1_SCHEMA } from "./1"
export const V2_SCHEMA = V1_SCHEMA.extend({
v: z.literal(2),
variables: z.array(
z.object({
key: z.string(),
initialValue: z.string(),
currentValue: z.string(),
secret: z.boolean(),
})
),
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
const result: z.infer<typeof V2_SCHEMA> = {
...old,
v: 2,
variables: old.variables.map((variable) => {
// if the variable is secret, set initialValue and currentValue to empty string
// else set initialValue and currentValue to value
// and delete value
return {
...variable,
initialValue: variable.secret ? "" : variable.value,
currentValue: variable.secret ? "" : variable.value,
value: undefined,
}
}),
}
return result
},
})

View file

@ -11,13 +11,27 @@ describe("Base64 helper functions", () => {
atob: {
script: `pw.env.set("atob", atob("SGVsbG8gV29ybGQ="))`,
environment: {
selected: [{ key: "atob", value: "Hello World", secret: false }],
selected: [
{
key: "atob",
currentValue: "Hello World",
initialValue: "Hello World",
secret: false,
},
],
},
},
btoa: {
script: `pw.env.set("btoa", btoa("Hello World"))`,
environment: {
selected: [{ key: "btoa", value: "SGVsbG8gV29ybGQ=", secret: false }],
selected: [
{
key: "btoa",
currentValue: "SGVsbG8gV29ybGQ=",
initialValue: "SGVsbG8gV29ybGQ=",
secret: false,
},
],
},
},
}

View file

@ -31,7 +31,8 @@ describe("pw.env.get", () => {
selected: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -60,7 +61,8 @@ describe("pw.env.get", () => {
global: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -114,14 +116,16 @@ describe("pw.env.get", () => {
global: [
{
key: "a",
value: "global val",
currentValue: "global val",
initialValue: "global val",
secret: false,
},
],
selected: [
{
key: "a",
value: "selected val",
currentValue: "selected val",
initialValue: "selected val",
secret: false,
},
],
@ -151,7 +155,8 @@ describe("pw.env.get", () => {
selected: [
{
key: "a",
value: "<<hello>>",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
],

View file

@ -31,7 +31,8 @@ describe("pw.env.getResolve", () => {
selected: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -60,7 +61,8 @@ describe("pw.env.getResolve", () => {
global: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -114,14 +116,16 @@ describe("pw.env.getResolve", () => {
global: [
{
key: "a",
value: "global val",
currentValue: "global val",
initialValue: "global val",
secret: false,
},
],
selected: [
{
key: "a",
value: "selected val",
currentValue: "selected val",
initialValue: "selected val",
secret: false,
},
],
@ -151,12 +155,14 @@ describe("pw.env.getResolve", () => {
selected: [
{
key: "a",
value: "<<hello>>",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
{
key: "hello",
value: "there",
currentValue: "there",
initialValue: "there",
secret: false,
},
],
@ -186,12 +192,14 @@ describe("pw.env.getResolve", () => {
selected: [
{
key: "a",
value: "<<hello>>",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
{
key: "hello",
value: "<<a>>",
currentValue: "<<a>>",
initialValue: "<<a>>",
secret: false,
},
],

View file

@ -44,7 +44,8 @@ describe("pw.env.resolve", () => {
global: [
{
key: "hello",
value: "there",
initialValue: "there",
currentValue: "there",
secret: false,
},
],
@ -75,7 +76,8 @@ describe("pw.env.resolve", () => {
selected: [
{
key: "hello",
value: "there",
initialValue: "there",
currentValue: "there",
secret: false,
},
],
@ -104,14 +106,16 @@ describe("pw.env.resolve", () => {
global: [
{
key: "hello",
value: "yo",
initialValue: "yo",
currentValue: "yo",
secret: false,
},
],
selected: [
{
key: "hello",
value: "there",
initialValue: "there",
currentValue: "there",
secret: false,
},
],
@ -141,12 +145,14 @@ describe("pw.env.resolve", () => {
selected: [
{
key: "hello",
value: "<<there>>",
currentValue: "<<there>>",
initialValue: "<<there>>",
secret: false,
},
{
key: "there",
value: "<<hello>>",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
],

View file

@ -36,7 +36,8 @@ describe("pw.env.set", () => {
selected: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -47,7 +48,8 @@ describe("pw.env.set", () => {
selected: [
{
key: "a",
value: "c",
currentValue: "c",
initialValue: "b",
secret: false,
},
],
@ -65,7 +67,8 @@ describe("pw.env.set", () => {
global: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
@ -77,7 +80,8 @@ describe("pw.env.set", () => {
global: [
{
key: "a",
value: "c",
currentValue: "c",
initialValue: "b",
secret: false,
},
],
@ -95,14 +99,16 @@ describe("pw.env.set", () => {
global: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
selected: [
{
key: "a",
value: "d",
currentValue: "d",
initialValue: "d",
secret: false,
},
],
@ -113,14 +119,16 @@ describe("pw.env.set", () => {
global: [
{
key: "a",
value: "b",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
selected: [
{
key: "a",
value: "c",
currentValue: "c",
initialValue: "d",
secret: false,
},
],
@ -145,7 +153,8 @@ describe("pw.env.set", () => {
selected: [
{
key: "a",
value: "c",
currentValue: "c",
initialValue: "c",
secret: false,
},
],

View file

@ -36,7 +36,8 @@ describe("pw.env.unset", () => {
selected: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -59,7 +60,8 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -83,14 +85,16 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://httpbin.org",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
],
selected: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -101,7 +105,8 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://httpbin.org",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
],
@ -120,19 +125,22 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [
{
key: "baseUrl",
value: "https://httpbin.org",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -143,14 +151,16 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -168,12 +178,14 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://httpbin.org/",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -185,7 +197,8 @@ describe("pw.env.unset", () => {
global: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
@ -239,7 +252,8 @@ describe("pw.env.unset", () => {
selected: [
{
key: "baseUrl",
value: "https://echo.hoppscotch.io",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],

View file

@ -12,16 +12,36 @@ describe("runPreRequestScript", () => {
{
global: [],
selected: [
{ key: "bob", value: "oldbob", secret: false },
{ key: "foo", value: "bar", secret: false },
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toEqualRight({
global: [],
selected: [
{ key: "bob", value: "newbob", secret: false },
{ key: "foo", value: "bar", secret: false },
{
key: "bob",
currentValue: "newbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
})
})
@ -35,8 +55,18 @@ describe("runPreRequestScript", () => {
{
global: [],
selected: [
{ key: "bob", value: "oldbob", secret: false },
{ key: "foo", value: "bar", secret: false },
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
@ -52,8 +82,18 @@ describe("runPreRequestScript", () => {
{
global: [],
selected: [
{ key: "bob", value: "oldbob", secret: false },
{ key: "foo", value: "bar", secret: false },
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
@ -69,8 +109,18 @@ describe("runPreRequestScript", () => {
{
global: [],
selected: [
{ key: "bob", value: "oldbob", secret: false },
{ key: "foo", value: "bar", secret: false },
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
@ -87,7 +137,9 @@ describe("runPreRequestScript", () => {
)()
).resolves.toEqualRight({
global: [],
selected: [{ key: "foo", value: "bar", secret: false }],
selected: [
{ key: "foo", currentValue: "bar", initialValue: "bar", secret: false },
],
})
})
})

View file

@ -39,16 +39,18 @@ const setEnv = (
if (indexInSelected >= 0) {
const selectedEnv = selected[indexInSelected]
if ("value" in selectedEnv) {
selectedEnv.value = envValue
if ("currentValue" in selectedEnv) {
selectedEnv.currentValue = envValue
}
} else if (indexInGlobal >= 0) {
if ("value" in global[indexInGlobal])
(global[indexInGlobal] as { value: string }).value = envValue
if ("currentValue" in global[indexInGlobal])
(global[indexInGlobal] as { currentValue: string }).currentValue =
envValue
} else {
selected.push({
key: envName,
value: envValue,
currentValue: envValue,
initialValue: envValue,
secret: false,
})
}
@ -93,7 +95,7 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
getEnv(key, updatedEnvs),
O.fold(
() => undefined,
(env) => String(env.value)
(env) => String(env.currentValue)
)
)
@ -111,11 +113,11 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
E.map((e) =>
pipe(
parseTemplateStringE(e.value, [
parseTemplateStringE(e.currentValue, [
...updatedEnvs.selected,
...updatedEnvs.global,
]), // If the recursive resolution failed, return the unresolved value
E.getOrElse(() => e.value)
E.getOrElse(() => e.currentValue)
)
),
E.map((x) => String(x)),

View file

@ -44,7 +44,8 @@ export type TestDescriptor = {
// Representation of a transformed state for environment variables in the sandbox
type TransformedEnvironmentVariable = {
key: string
value: string
currentValue: string
initialValue: string
secret: boolean
}

View file

@ -84,7 +84,7 @@ async function loadUserEnvironments() {
runDispatchWithOutSyncing(() => {
replaceEnvironments(
environments.map(({ id, variables, name }) => ({
v: 1,
v: 2,
id,
name,
variables: JSON.parse(variables),
@ -176,7 +176,7 @@ function setupUserEnvironmentUpdatedSubscription() {
if ((localIndex || localIndex == 0) && name) {
runDispatchWithOutSyncing(() => {
updateEnvironment(localIndex, {
v: 1,
v: 2,
id,
name,
variables: JSON.parse(variables),

View file

@ -110,7 +110,7 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
name: "",
variables: entries,
id: "",
v: 1,
v: 2,
})()
}
},

View file

@ -84,7 +84,7 @@ async function loadUserEnvironments() {
runDispatchWithOutSyncing(() => {
replaceEnvironments(
environments.map(({ id, variables, name }) => ({
v: 1,
v: 2,
id,
name,
variables: JSON.parse(variables),
@ -176,7 +176,7 @@ function setupUserEnvironmentUpdatedSubscription() {
if ((localIndex || localIndex == 0) && name) {
runDispatchWithOutSyncing(() => {
updateEnvironment(localIndex, {
v: 1,
v: 2,
id,
name,
variables: JSON.parse(variables),

View file

@ -110,7 +110,7 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
name: "",
variables: entries,
id: "",
v: 1,
v: 2,
})()
}
},