@zambelz/zhc
Version:
API Management Tools
380 lines (319 loc) • 9.63 kB
text/typescript
import path from "node:path"
import fs from "node:fs"
import { PROFILES_PATH } from "../../../utils/global"
import { parseContent } from "../../../utils/common"
import { getConfigData } from "../../../utils/config"
import { logError, logInfo } from "../../../utils/logger"
import runCustomScript from "../shared/scriptExecutor"
import { HTTPMethod } from "./restHttpMethod"
import {
execPostRequestScript,
execPreRequestScript
} from "../shared/requestScript"
const getEnvData = (
profile?: string,
env?: string
): { path: string, content: string } | undefined => {
try {
const configData = getConfigData()
const targetProfile = profile || configData?.defaultProfile
const envDirPath = path.join(PROFILES_PATH, targetProfile, "env")
const targetEnv = `${env || "default"}.jsonc`
const envFilePath = path.join(envDirPath, targetEnv)
const envContent = fs.readFileSync(envFilePath, "utf-8")
return {
path: envFilePath,
content: envContent,
}
} catch (err) {
throw new Error(`Failed to get env data: ${err}`)
}
}
const getEndpointData = (
name: string,
profile?: string
): { path: string, data: Record<string, any> } | undefined => {
if (!name || name.trim() === "") {
throw new Error("Name is required")
}
try {
const configData = getConfigData()
const targetProfile = profile || configData?.defaultProfile
const endpointsDirPath = path.join(
PROFILES_PATH, targetProfile, "endpoints"
)
const targetEndpointPath = name.split(":").slice(0, -1).join("/")
const targetEndpointName = name.split(":").slice(-1)[0]
const endpointFilePath = path.join(
endpointsDirPath, `${targetEndpointPath || "default"}.jsonc`
)
const endpointContent = fs.readFileSync(endpointFilePath, "utf-8")
const content = parseContent(endpointContent)
const data = Object.entries(content.parsed)
.filter(([key]) => key === targetEndpointName)[0]
return {
path: endpointFilePath,
data: data[1] as Record<string, any>
}
} catch (err) {
logError(`${err}`)
return undefined
}
}
const assignValueFromVar = (
src: Record<string, any>,
target: Record<string, any>
) => {
let result = { ...target }
const hasBracket = (str: string) => {
return str[0] === "{" && str[1] === "{"
&& str[str.length - 1] === "}" && str[str.length - 2] === "}"
}
for (const [key, value] of Object.entries(target)) {
if (!hasBracket(value)) {
continue
}
const paramKey = value.slice(2, value.length - 2)
if (src[paramKey]) {
result[key] = src[paramKey]
} else {
result[key] = ""
}
}
return result
}
const assignValueFromArgs = (
target: Record<string, any>,
args?: string
) => {
let result = { ...target }
if (!args || args.trim() === "") {
return result
}
const argObjects = args.split(":").map((arg) => {
const [key, value] = arg.split("=")
return { [key]: value }
})
for (const arg of argObjects) {
const key = Object.keys(arg)[0]
const value = Object.values(arg)[0]
if (target[key]) {
result[key] = value
}
}
return result
}
const assignValueFromScript = (target: Record<string, any>) => {
let result = { ...target }
const hasScriptSign = (str: string) => {
return str[0] === "<" && str[1] === "%"
&& str[str.length - 2] === "%" && str[str.length - 1] === ">"
}
const getScriptName = (key: string): string => {
const openParenthesisIndex = key.indexOf("(")
return key.slice(2, openParenthesisIndex)
}
const getArgs = (key: string): string[] => {
const openParenthesisIndex = key.indexOf("(")
const closeParenthesisIndex = key.indexOf(")")
return key.slice(openParenthesisIndex + 1, closeParenthesisIndex)
.split(",")
}
for (const [key, value] of Object.entries(target)) {
if (!hasScriptSign(value)) {
continue
}
const scriptName = getScriptName(value)
const scriptFn = value.slice(2, value.length - 2)
const args = getArgs(scriptFn)
const valueFromScript = runCustomScript(scriptName, ...args)
if (valueFromScript) {
result[key] = valueFromScript
}
}
return result
}
const assignEndpointVariables = (
src: Record<string, unknown>,
endpoint: string
) => {
const endpointVariables = endpoint.match(/\{\{(.+?)\}\}/g)
if (!endpointVariables) {
return endpoint
}
let result = endpoint
for (const variable of endpointVariables) {
const variableName = variable.slice(2, variable.length - 2)
const variableValue = src[variableName]
if (!variableValue) {
continue
}
const formattedVariable = `${variableValue}`.trim().replace(/\s+/g, " ")
const finalVariable = formattedVariable.replaceAll(/\s/g, "%20")
result = result.replace(variable, finalVariable)
}
return result
}
const configureRequest = (
envData: Record<string, any> | undefined,
endpointData: Record<string, any> | undefined,
additionalArgs: string
): {
method: HTTPMethod,
finalURL: string,
requestData: Record<string, any>
} => {
if (!envData || !endpointData) {
logError("Environment or endpoint data not found")
return { method: "GET", finalURL: "", requestData: {} }
}
const endpointPath = assignEndpointVariables(envData, endpointData["path"])
const fullURL = `${envData.protocol}://${envData.baseURL}${endpointPath}`
const method: HTTPMethod = endpointData["method"]
const headers: Record<string, string> = assignValueFromScript(
assignValueFromArgs(
assignValueFromVar(envData, endpointData["headers"]),
additionalArgs
)
)
const params = assignValueFromScript(
assignValueFromArgs(
assignValueFromVar(envData, endpointData["params"]),
additionalArgs
)
)
let finalURL = fullURL
let requestData: Record<string, any> = {}
switch (method) {
case "GET":
// TODO: Need to revisit this part
finalURL = params && Object.keys(params).length > 0
? `${fullURL}?${new URLSearchParams(params).toString()}`
: fullURL
requestData = {
method,
headers
}
break
case "POST":
case "PUT":
case "PATCH":
requestData = {
method,
headers,
body: params ? params : ""
}
break
case "DELETE":
requestData = {
method,
headers
}
break
}
return { method, finalURL, requestData }
}
const constructResponse = async (
method: string,
response: Response
) => {
let responseHeaders: Record<string, string> = {}
for (const [key, value] of response.headers.entries()) {
responseHeaders[key] = value
}
const contentType = responseHeaders["content-type"]
if (contentType.includes("text/html")) {
return await response.text()
}
const result: Record<string, unknown> = {}
if (response.status > 499) {
result["status"] = response.status
result["error"] = response.statusText
return result
}
const responseBody = await response.json()
result[method] = response.url
result["status"] = response.status
result["headers"] = responseHeaders
result["cookie"] = response.headers.getSetCookie()
result["body"] = responseBody
return result
}
const httpRequest = async (opt: Record<string, string | boolean>) => {
const verbose = opt?.verbose as boolean
try {
const env = getEnvData(opt?.profile as string, opt?.env as string)
if (!env) {
logError("Environment data not found")
return
}
const envContent = parseContent(env.content)
const envData = envContent.parsed
const protocol = envData.protocol as string
const baseURL = envData.baseURL as string
if (!protocol || !baseURL) {
logError("Base URL not found")
return
}
const name = opt?.call as string
const endpoint = getEndpointData(name, opt?.profile as string)
if (!endpoint) {
logError("Endpoint data not found")
return
}
const endpointData = endpoint.data
const preRequestScript = endpointData.scripts?.pre as string
let updatedEnv: Record<string, any> = envData
if (preRequestScript) {
updatedEnv = execPreRequestScript(preRequestScript, env, verbose)
}
const additionalArgs = opt?.args as string
const { method, finalURL, requestData } = configureRequest(
envData,
endpointData,
additionalArgs
)
if (verbose) {
logInfo("[REST] Request Payload:")
console.log({ destination: finalURL, ...requestData })
console.log()
}
const response = await fetch(
finalURL,
{
...requestData,
body: JSON.stringify(requestData.body)
} as RequestInit
)
const httpResponse = await constructResponse(method, response)
if (typeof httpResponse === "string") {
return httpResponse
}
const postRequestScript = endpointData.scripts?.post as string
if (postRequestScript) {
execPostRequestScript(
postRequestScript,
{
path: env.path,
unparseable: envContent.unparseable,
parsed: updatedEnv
},
httpResponse,
verbose
)
}
return httpResponse
} catch (err) {
logError(`${err}`)
}
}
export default async function executeRestApi(
options: Record<string, string | boolean>
) {
console.time("[REST] API Request Time")
const response = await httpRequest(options)
console.log("[REST] API Response:")
console.dir(response, { depth: 5 })
console.log()
console.timeEnd("[REST] API Request Time")
}