Files
hoppscotch/packages/hoppscotch-cli/src/utils/pre-request.ts

291 lines
8.9 KiB
TypeScript

import {
Environment,
HoppRESTRequest,
parseBodyEnvVariablesE,
parseRawKeyValueEntriesE,
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { toFormData } from "./mutators";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and
* applies them on current request to generate updated request.
* @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest.
* @param envs Environment variables related to request.
* @returns EffectiveHoppRESTRequest that includes parsed ENV variables with in
* request OR HoppCLIError with error code and related information.
*/
export const preRequestScriptRunner = (
request: HoppRESTRequest,
envs: HoppEnvs
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
pipe(
TE.of(request),
TE.chain(({ preRequestScript }) =>
runPreRequestScript(preRequestScript, envs)
),
TE.map(
({ selected, global }) =>
<Environment>{ name: "Env", variables: [...selected, ...global] }
),
TE.chainEitherKW((env) => getEffectiveRESTRequest(request, env)),
TE.mapLeft((reason) =>
isHoppCLIError(reason)
? reason
: error({
code: "PRE_REQUEST_SCRIPT_ERROR",
data: reason,
})
)
);
/**
* Outputs an executable request format with environment variables applied
*
* @param request The request to source from
* @param environment The environment to apply
*
* @returns An object with extra fields defining a complete request
*/
export function getEffectiveRESTRequest(
request: HoppRESTRequest,
environment: Environment
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
const envVariables = environment.variables;
// Parsing final headers with applied ENVs.
const _effectiveFinalHeaders = getEffectiveFinalMetaData(
request.headers,
environment
);
if (E.isLeft(_effectiveFinalHeaders)) {
return _effectiveFinalHeaders;
}
const effectiveFinalHeaders = _effectiveFinalHeaders.right;
// Parsing final parameters with applied ENVs.
const _effectiveFinalParams = getEffectiveFinalMetaData(
request.params,
environment
);
if (E.isLeft(_effectiveFinalParams)) {
return _effectiveFinalParams;
}
const effectiveFinalParams = _effectiveFinalParams.right;
// Authentication
if (request.auth.authActive) {
// TODO: Support a better b64 implementation than btoa ?
if (request.auth.authType === "basic") {
const username = parseTemplateString(request.auth.username, envVariables);
const password = parseTemplateString(request.auth.password, envVariables);
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
});
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(
request.auth.token,
envVariables
)}`,
});
} else if (request.auth.authType === "api-key") {
const { key, value, addTo } = request.auth;
if (addTo === "Headers") {
effectiveFinalHeaders.push({
active: true,
key: parseTemplateString(key, envVariables),
value: parseTemplateString(value, envVariables),
});
} else if (addTo === "Query params") {
effectiveFinalParams.push({
active: true,
key: parseTemplateString(key, envVariables),
value: parseTemplateString(value, envVariables),
});
}
}
}
// Parsing final-body with applied ENVs.
const _effectiveFinalBody = getFinalBodyFromRequest(request, envVariables);
if (E.isLeft(_effectiveFinalBody)) {
return _effectiveFinalBody;
}
const effectiveFinalBody = _effectiveFinalBody.right;
if (request.body.contentType)
effectiveFinalHeaders.push({
active: true,
key: "content-type",
value: request.body.contentType,
});
// Parsing final-endpoint with applied ENVs.
const _effectiveFinalURL = parseTemplateStringE(
request.endpoint,
envVariables
);
if (E.isLeft(_effectiveFinalURL)) {
return E.left(
error({
code: "PARSING_ERROR",
data: `${request.endpoint} (${_effectiveFinalURL.left})`,
})
);
}
const effectiveFinalURL = _effectiveFinalURL.right;
return E.right({
...request,
effectiveFinalURL,
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
});
}
/**
* Replaces template variables in request's body from the given set of ENVs,
* to generate final request body without any template variables.
* @param request Provides request's body, on which ENVs has to be applied.
* @param envVariables Provides set of key-value pairs (environment variables),
* used to parse-out template variables.
* @returns Final request body without any template variables as value.
* Or, HoppCLIError in case of error while parsing.
*/
function getFinalBodyFromRequest(
request: HoppRESTRequest,
envVariables: Environment["variables"]
): E.Either<HoppCLIError, string | null | FormData> {
if (request.body.contentType === null) {
return E.right(null);
}
if (request.body.contentType === "application/x-www-form-urlencoded") {
return pipe(
request.body.body,
parseRawKeyValueEntriesE,
E.map(
flow(
RA.toArray,
/**
* Filtering out empty keys and non-active pairs.
*/
A.filter(({ active, key }) => active && !S.isEmpty(key)),
/**
* Mapping each key-value to template-string-parser with either on array,
* which will be resolved in further steps.
*/
A.map(({ key, value }) => [
parseTemplateStringE(key, envVariables),
parseTemplateStringE(value, envVariables),
]),
/**
* Filtering and mapping only right-eithers for each key-value as [string, string].
*/
A.filterMap(([key, value]) =>
E.isRight(key) && E.isRight(value)
? O.some([key.right, value.right] as [string, string])
: O.none
),
tupleToRecord,
qs.stringify
)
),
E.mapLeft((e) => error({ code: "PARSING_ERROR", data: e.message }))
);
}
if (request.body.contentType === "multipart/form-data") {
return pipe(
request.body.body,
A.filter((x) => x.key !== "" && x.active), // Remove empty keys
// Sort files down
arraySort((a, b) => {
if (a.isFile) return 1;
if (b.isFile) return -1;
return 0;
}),
// FormData allows only a single blob in an entry,
// we split array blobs into separate entries (FormData will then join them together during exec)
arrayFlatMap((x) =>
x.isFile
? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
}))
: [
{
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
},
]
),
toFormData,
E.right
);
}
return pipe(
parseBodyEnvVariablesE(request.body.body, envVariables),
E.mapLeft((e) =>
error({
code: "PARSING_ERROR",
data: `${request.body.body} (${e})`,
})
)
);
}
/**
* Get pre-request-metrics (stats + duration) object based on existence of
* PRE_REQUEST_ERROR code in given hopp-error list.
* @param errors List of errors to check for PRE_REQUEST_ERROR code.
* @param duration Time taken (in seconds) to execute the pre-request-script.
* @returns Object containing details of pre-request-script's execution stats
* i.e., failed/passed data and duration.
*/
export const getPreRequestMetrics = (
errors: HoppCLIError[],
duration: number
): PreRequestMetrics =>
pipe(
errors,
A.some(({ code }) => code === "PRE_REQUEST_SCRIPT_ERROR"),
(hasPreReqErrors) =>
hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(scripts) => <PreRequestMetrics>{ scripts, duration }
);