Files
hoppscotch/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts

378 lines
12 KiB
TypeScript

import { getI18n } from "~/modules/i18n"
import {
InspectionService,
Inspector,
InspectorLocation,
InspectorResult,
} from ".."
import { Service } from "dioc"
import { Ref, markRaw } from "vue"
import IconPlusCircle from "~icons/lucide/plus-circle"
import { HoppRESTRequest } from "@hoppscotch/data"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
getCurrentEnvironment,
getSelectedEnvironmentType,
} from "~/newstore/environments"
import { invokeAction } from "~/helpers/actions"
import { computed } from "vue"
import { useStreamStatic } from "~/composables/stream"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const isENVInString = (str: string) => {
return HOPP_ENVIRONMENT_REGEX.test(str)
}
/**
* This inspector is responsible for inspecting the environment variables of a input.
* It checks if the environment variables are defined in the environment.
* It also provides an action to add the environment variable.
*
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
*/
export class EnvironmentInspectorService extends Service implements Inspector {
public static readonly ID = "ENVIRONMENT_INSPECTOR_SERVICE"
private t = getI18n()
public readonly inspectorID = "environment"
private readonly inspection = this.bind(InspectionService)
private readonly secretEnvs = this.bind(SecretEnvironmentService)
private readonly restTabs = this.bind(RESTTabService)
private aggregateEnvsWithSecrets = useStreamStatic(
aggregateEnvsWithSecrets$,
[],
() => {
/* noop */
}
)[0]
constructor() {
super()
this.inspection.registerInspector(this)
}
/**
* Validates the environment variables in the target array
* @param target The target array to validate
* @param locations The location where results are to be displayed
* @returns The results array containing the results of the validation
*/
private validateEnvironmentVariables = (
target: any[],
locations: InspectorLocation
) => {
const newErrors: InspectorResult[] = []
const currentTab = this.restTabs.currentActiveTab.value
const environmentVariables = [
...currentTab.document.request.requestVariables,
...this.aggregateEnvsWithSecrets.value,
]
const envKeys = environmentVariables.map((e) => e.key)
target.forEach((element, index) => {
if (isENVInString(element)) {
const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
if (extractedEnv) {
extractedEnv.forEach((exEnv: string) => {
const formattedExEnv = exEnv.slice(2, -2)
const itemLocation: InspectorLocation = {
type: locations.type,
position:
locations.type === "url" ||
locations.type === "body" ||
locations.type === "response"
? "key"
: locations.position,
index: index,
key: element,
}
if (!envKeys.includes(formattedExEnv)) {
newErrors.push({
id: `environment-not-found-${newErrors.length}`,
text: {
type: "text",
text: this.t("inspections.environment.not_found", {
environment: exEnv,
}),
},
icon: markRaw(IconPlusCircle),
action: {
text: this.t("inspections.environment.add_environment"),
apply: () => {
invokeAction("modals.environment.add", {
envName: formattedExEnv,
variableName: "",
})
},
},
severity: 3,
isApplicable: true,
locations: itemLocation,
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/documentation/features/inspections",
},
})
}
})
}
}
})
return newErrors
}
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
private filterNonEmptyEnvironmentVariables = (
envs: AggregateEnvironment[]
): AggregateEnvironment[] => {
const envsMap = new Map<string, AggregateEnvironment>()
envs.forEach((env) => {
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (existingEnv?.value === "" && env.value !== "") {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
/**
* Checks if the environment variables in the target array are empty
* @param target The target array to validate
* @param locations The location where results are to be displayed
* @returns The results array containing the results of the validation
*/
private validateEmptyEnvironmentVariables = (
target: any[],
locations: InspectorLocation
) => {
const newErrors: InspectorResult[] = []
target.forEach((element, index) => {
if (isENVInString(element)) {
const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
if (extractedEnv) {
extractedEnv.forEach((exEnv: string) => {
const formattedExEnv = exEnv.slice(2, -2)
const currentSelectedEnvironment = getCurrentEnvironment()
const currentTab = this.restTabs.currentActiveTab.value
const environmentVariables =
this.filterNonEmptyEnvironmentVariables([
...currentTab.document.request.requestVariables.map((env) => ({
...env,
secret: false,
sourceEnv: "RequestVariable",
})),
...this.aggregateEnvsWithSecrets.value,
])
environmentVariables.forEach((env) => {
const hasSecretEnv = this.secretEnvs.hasSecretValue(
env.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
env.key
)
if (env.key === formattedExEnv) {
if (env.secret ? !hasSecretEnv : env.value === "") {
const itemLocation: InspectorLocation = {
type: locations.type,
position:
locations.type === "url" ||
locations.type === "body" ||
locations.type === "response"
? "key"
: locations.position,
index: index,
key: element,
}
const currentEnvironmentType = getSelectedEnvironmentType()
let invokeActionType:
| "modals.my.environment.edit"
| "modals.team.environment.edit"
| "modals.global.environment.update" =
"modals.my.environment.edit"
if (env.sourceEnv === "Global") {
invokeActionType = "modals.global.environment.update"
} else if (currentEnvironmentType === "MY_ENV") {
invokeActionType = "modals.my.environment.edit"
} else if (currentEnvironmentType === "TEAM_ENV") {
invokeActionType = "modals.team.environment.edit"
} else {
invokeActionType = "modals.my.environment.edit"
}
newErrors.push({
id: `environment-empty-${newErrors.length}`,
text: {
type: "text",
text: this.t("inspections.environment.empty_value", {
variable: exEnv,
}),
},
icon: markRaw(IconPlusCircle),
action: {
text: this.t(
"inspections.environment.add_environment_value"
),
apply: () => {
if (env.sourceEnv === "RequestVariable") {
currentTab.document.optionTabPreference =
"requestVariables"
} else {
invokeAction(invokeActionType, {
envName:
env.sourceEnv === "Global"
? "Global"
: currentSelectedEnvironment.name,
variableName: formattedExEnv,
isSecret: env.secret,
})
}
},
},
severity: 2,
isApplicable: true,
locations: itemLocation,
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/documentation/features/inspections",
},
})
}
}
})
})
}
}
})
return newErrors
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
return computed(() => {
const results: InspectorResult[] = []
const headers = req.value.headers
const params = req.value.params
/**
* Validate the environment variables in the URL
*/
const url = req.value.endpoint
results.push(
...this.validateEnvironmentVariables([url], {
type: "url",
})
)
results.push(
...this.validateEmptyEnvironmentVariables([url], {
type: "url",
})
)
/**
* Validate the environment variables in the headers
*/
const headerKeys = Object.values(headers).map((header) => header.key)
results.push(
...this.validateEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
})
)
results.push(
...this.validateEmptyEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
})
)
const headerValues = Object.values(headers).map((header) => header.value)
results.push(
...this.validateEnvironmentVariables(headerValues, {
type: "header",
position: "value",
})
)
results.push(
...this.validateEmptyEnvironmentVariables(headerValues, {
type: "header",
position: "value",
})
)
/**
* Validate the environment variables in the parameters
*/
const paramsKeys = Object.values(params).map((param) => param.key)
results.push(
...this.validateEnvironmentVariables(paramsKeys, {
type: "parameter",
position: "key",
})
)
results.push(
...this.validateEmptyEnvironmentVariables(paramsKeys, {
type: "parameter",
position: "key",
})
)
const paramsValues = Object.values(params).map((param) => param.value)
results.push(
...this.validateEnvironmentVariables(paramsValues, {
type: "parameter",
position: "value",
})
)
results.push(
...this.validateEmptyEnvironmentVariables(paramsValues, {
type: "parameter",
position: "value",
})
)
return results
})
}
}