EffectiveURL.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import * as A from "fp-ts/Array"
  2. import qs from "qs"
  3. import { pipe } from "fp-ts/function"
  4. import { combineLatest, Observable } from "rxjs"
  5. import { map } from "rxjs/operators"
  6. import {
  7. FormDataKeyValue,
  8. HoppRESTReqBody,
  9. HoppRESTRequest,
  10. parseTemplateString,
  11. parseBodyEnvVariables,
  12. parseRawKeyValueEntries,
  13. Environment,
  14. } from "@hoppscotch/data"
  15. import { arrayFlatMap, arraySort } from "../functional/array"
  16. import { toFormData } from "../functional/formData"
  17. import { tupleToRecord } from "../functional/record"
  18. import { getGlobalVariables } from "~/newstore/environments"
  19. export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
  20. /**
  21. * The effective final URL.
  22. *
  23. * This contains path, params and environment variables all applied to it
  24. */
  25. effectiveFinalURL: string
  26. effectiveFinalHeaders: { key: string; value: string }[]
  27. effectiveFinalParams: { key: string; value: string }[]
  28. effectiveFinalBody: FormData | string | null
  29. }
  30. // Resolves environment variables in the body
  31. export const resolvesEnvsInBody = (
  32. body: HoppRESTReqBody,
  33. env: Environment
  34. ): HoppRESTReqBody => {
  35. if (!body.contentType) return body
  36. if (body.contentType === "multipart/form-data") {
  37. return {
  38. contentType: "multipart/form-data",
  39. body: body.body.map(
  40. (entry) =>
  41. <FormDataKeyValue>{
  42. active: entry.active,
  43. isFile: entry.isFile,
  44. key: parseTemplateString(entry.key, env.variables),
  45. value: entry.isFile
  46. ? entry.value
  47. : parseTemplateString(entry.value, env.variables),
  48. }
  49. ),
  50. }
  51. } else {
  52. return {
  53. contentType: body.contentType,
  54. body: parseTemplateString(body.body, env.variables),
  55. }
  56. }
  57. }
  58. function getFinalBodyFromRequest(
  59. request: HoppRESTRequest,
  60. envVariables: Environment["variables"]
  61. ): FormData | string | null {
  62. if (request.body.contentType === null) {
  63. return null
  64. }
  65. if (request.body.contentType === "application/x-www-form-urlencoded") {
  66. return pipe(
  67. request.body.body,
  68. parseRawKeyValueEntries,
  69. // Filter out active
  70. A.filter((x) => x.active),
  71. // Convert to tuple
  72. A.map(
  73. ({ key, value }) =>
  74. [
  75. parseTemplateString(key, envVariables),
  76. parseTemplateString(value, envVariables),
  77. ] as [string, string]
  78. ),
  79. // Tuple to Record object
  80. tupleToRecord,
  81. // Stringify
  82. qs.stringify
  83. )
  84. }
  85. if (request.body.contentType === "multipart/form-data") {
  86. return pipe(
  87. request.body.body,
  88. A.filter((x) => x.key !== "" && x.active), // Remove empty keys
  89. // Sort files down
  90. arraySort((a, b) => {
  91. if (a.isFile) return 1
  92. if (b.isFile) return -1
  93. return 0
  94. }),
  95. // FormData allows only a single blob in an entry,
  96. // we split array blobs into separate entries (FormData will then join them together during exec)
  97. arrayFlatMap((x) =>
  98. x.isFile
  99. ? x.value.map((v) => ({
  100. key: parseTemplateString(x.key, envVariables),
  101. value: v as string | Blob,
  102. }))
  103. : [
  104. {
  105. key: parseTemplateString(x.key, envVariables),
  106. value: parseTemplateString(x.value, envVariables),
  107. },
  108. ]
  109. ),
  110. toFormData
  111. )
  112. } else return parseBodyEnvVariables(request.body.body, envVariables)
  113. }
  114. /**
  115. * Outputs an executable request format with environment variables applied
  116. *
  117. * @param request The request to source from
  118. * @param environment The environment to apply
  119. *
  120. * @returns An object with extra fields defining a complete request
  121. */
  122. export function getEffectiveRESTRequest(
  123. request: HoppRESTRequest,
  124. environment: Environment
  125. ): EffectiveHoppRESTRequest {
  126. const envVariables = [...environment.variables, ...getGlobalVariables()]
  127. const effectiveFinalHeaders = request.headers
  128. .filter(
  129. (x) =>
  130. x.key !== "" && // Remove empty keys
  131. x.active // Only active
  132. )
  133. .map((x) => ({
  134. // Parse out environment template strings
  135. active: true,
  136. key: parseTemplateString(x.key, envVariables),
  137. value: parseTemplateString(x.value, envVariables),
  138. }))
  139. const effectiveFinalParams = request.params
  140. .filter(
  141. (x) =>
  142. x.key !== "" && // Remove empty keys
  143. x.active // Only active
  144. )
  145. .map((x) => ({
  146. active: true,
  147. key: parseTemplateString(x.key, envVariables),
  148. value: parseTemplateString(x.value, envVariables),
  149. }))
  150. // Authentication
  151. if (request.auth.authActive) {
  152. // TODO: Support a better b64 implementation than btoa ?
  153. if (request.auth.authType === "basic") {
  154. const username = parseTemplateString(request.auth.username, envVariables)
  155. const password = parseTemplateString(request.auth.password, envVariables)
  156. effectiveFinalHeaders.push({
  157. active: true,
  158. key: "Authorization",
  159. value: `Basic ${btoa(`${username}:${password}`)}`,
  160. })
  161. } else if (
  162. request.auth.authType === "bearer" ||
  163. request.auth.authType === "oauth-2"
  164. ) {
  165. effectiveFinalHeaders.push({
  166. active: true,
  167. key: "Authorization",
  168. value: `Bearer ${parseTemplateString(
  169. request.auth.token,
  170. envVariables
  171. )}`,
  172. })
  173. } else if (request.auth.authType === "api-key") {
  174. const { key, value, addTo } = request.auth
  175. if (addTo === "Headers") {
  176. effectiveFinalHeaders.push({
  177. active: true,
  178. key: parseTemplateString(key, envVariables),
  179. value: parseTemplateString(value, envVariables),
  180. })
  181. } else if (addTo === "Query params") {
  182. effectiveFinalParams.push({
  183. active: true,
  184. key: parseTemplateString(key, envVariables),
  185. value: parseTemplateString(value, envVariables),
  186. })
  187. }
  188. }
  189. }
  190. const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
  191. const contentTypeInHeader = effectiveFinalHeaders.find(
  192. (x) => x.key.toLowerCase() === "content-type"
  193. )
  194. if (request.body.contentType && !contentTypeInHeader?.value)
  195. effectiveFinalHeaders.push({
  196. active: true,
  197. key: "content-type",
  198. value: request.body.contentType,
  199. })
  200. return {
  201. ...request,
  202. effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
  203. effectiveFinalHeaders,
  204. effectiveFinalParams,
  205. effectiveFinalBody,
  206. }
  207. }
  208. /**
  209. * Creates an Observable Stream that emits HoppRESTRequests whenever
  210. * the input streams emit a value
  211. *
  212. * @param request$ The request stream containing request data
  213. * @param environment$ The environment stream containing environment data to apply
  214. *
  215. * @returns Observable Stream for the Effective Request Object
  216. */
  217. export function getEffectiveRESTRequestStream(
  218. request$: Observable<HoppRESTRequest>,
  219. environment$: Observable<Environment>
  220. ): Observable<EffectiveHoppRESTRequest> {
  221. return combineLatest([request$, environment$]).pipe(
  222. map(([request, env]) => getEffectiveRESTRequest(request, env))
  223. )
  224. }