123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import * as A from "fp-ts/Array"
- import qs from "qs"
- import { pipe } from "fp-ts/function"
- import { combineLatest, Observable } from "rxjs"
- import { map } from "rxjs/operators"
- import {
- FormDataKeyValue,
- HoppRESTReqBody,
- HoppRESTRequest,
- parseTemplateString,
- parseBodyEnvVariables,
- parseRawKeyValueEntries,
- Environment,
- } from "@hoppscotch/data"
- import { arrayFlatMap, arraySort } from "../functional/array"
- import { toFormData } from "../functional/formData"
- import { tupleToRecord } from "../functional/record"
- import { getGlobalVariables } from "~/newstore/environments"
- export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
- /**
- * The effective final URL.
- *
- * This contains path, params and environment variables all applied to it
- */
- effectiveFinalURL: string
- effectiveFinalHeaders: { key: string; value: string }[]
- effectiveFinalParams: { key: string; value: string }[]
- effectiveFinalBody: FormData | string | null
- }
- // Resolves environment variables in the body
- export const resolvesEnvsInBody = (
- body: HoppRESTReqBody,
- env: Environment
- ): HoppRESTReqBody => {
- if (!body.contentType) return body
- if (body.contentType === "multipart/form-data") {
- return {
- contentType: "multipart/form-data",
- body: body.body.map(
- (entry) =>
- <FormDataKeyValue>{
- active: entry.active,
- isFile: entry.isFile,
- key: parseTemplateString(entry.key, env.variables),
- value: entry.isFile
- ? entry.value
- : parseTemplateString(entry.value, env.variables),
- }
- ),
- }
- } else {
- return {
- contentType: body.contentType,
- body: parseTemplateString(body.body, env.variables),
- }
- }
- }
- function getFinalBodyFromRequest(
- request: HoppRESTRequest,
- envVariables: Environment["variables"]
- ): FormData | string | null {
- if (request.body.contentType === null) {
- return null
- }
- if (request.body.contentType === "application/x-www-form-urlencoded") {
- return pipe(
- request.body.body,
- parseRawKeyValueEntries,
- // Filter out active
- A.filter((x) => x.active),
- // Convert to tuple
- A.map(
- ({ key, value }) =>
- [
- parseTemplateString(key, envVariables),
- parseTemplateString(value, envVariables),
- ] as [string, string]
- ),
- // Tuple to Record object
- tupleToRecord,
- // Stringify
- qs.stringify
- )
- }
- 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
- )
- } else return parseBodyEnvVariables(request.body.body, envVariables)
- }
- /**
- * 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
- ): EffectiveHoppRESTRequest {
- const envVariables = [...environment.variables, ...getGlobalVariables()]
- const effectiveFinalHeaders = request.headers
- .filter(
- (x) =>
- x.key !== "" && // Remove empty keys
- x.active // Only active
- )
- .map((x) => ({
- // Parse out environment template strings
- active: true,
- key: parseTemplateString(x.key, envVariables),
- value: parseTemplateString(x.value, envVariables),
- }))
- const effectiveFinalParams = request.params
- .filter(
- (x) =>
- x.key !== "" && // Remove empty keys
- x.active // Only active
- )
- .map((x) => ({
- active: true,
- key: parseTemplateString(x.key, envVariables),
- value: parseTemplateString(x.value, envVariables),
- }))
- // 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),
- })
- }
- }
- }
- const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
- const contentTypeInHeader = effectiveFinalHeaders.find(
- (x) => x.key.toLowerCase() === "content-type"
- )
- if (request.body.contentType && !contentTypeInHeader?.value)
- effectiveFinalHeaders.push({
- active: true,
- key: "content-type",
- value: request.body.contentType,
- })
- return {
- ...request,
- effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
- effectiveFinalHeaders,
- effectiveFinalParams,
- effectiveFinalBody,
- }
- }
- /**
- * Creates an Observable Stream that emits HoppRESTRequests whenever
- * the input streams emit a value
- *
- * @param request$ The request stream containing request data
- * @param environment$ The environment stream containing environment data to apply
- *
- * @returns Observable Stream for the Effective Request Object
- */
- export function getEffectiveRESTRequestStream(
- request$: Observable<HoppRESTRequest>,
- environment$: Observable<Environment>
- ): Observable<EffectiveHoppRESTRequest> {
- return combineLatest([request$, environment$]).pipe(
- map(([request, env]) => getEffectiveRESTRequest(request, env))
- )
- }
|