body.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import parser from "yargs-parser"
  2. import { pipe, flow } from "fp-ts/function"
  3. import * as O from "fp-ts/Option"
  4. import * as A from "fp-ts/Array"
  5. import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
  6. import * as S from "fp-ts/string"
  7. import {
  8. HoppRESTReqBody,
  9. HoppRESTReqBodyFormData,
  10. ValidContentTypes,
  11. knownContentTypes,
  12. } from "@hoppscotch/data"
  13. import { detectContentType, parseBody } from "./contentParser"
  14. import { tupleToRecord } from "~/helpers/functional/record"
  15. import {
  16. objHasProperty,
  17. objHasArrayProperty,
  18. } from "~/helpers/functional/object"
  19. type BodyReturnType =
  20. | { type: "FORMDATA"; body: Record<string, string> }
  21. | {
  22. type: "NON_FORMDATA"
  23. body: Exclude<HoppRESTReqBody, HoppRESTReqBodyFormData>
  24. }
  25. /** Parses body based on the content type
  26. * @param rData Raw data
  27. * @param cType Sanitized content type
  28. * @returns Option of parsed body of type string | Record<string, string>
  29. */
  30. const getBodyFromContentType =
  31. (rData: string, cType: HoppRESTReqBody["contentType"]) => (rct: string) =>
  32. pipe(
  33. cType,
  34. O.fromPredicate((ctype) => ctype === "multipart/form-data"),
  35. O.chain(() =>
  36. pipe(
  37. // pass rawContentType for boundary ascertion
  38. parseBody(rData, cType, rct),
  39. O.filter((parsedBody) => typeof parsedBody !== "string")
  40. )
  41. ),
  42. O.alt(() =>
  43. pipe(
  44. parseBody(rData, cType),
  45. O.filter(
  46. (parsedBody) =>
  47. typeof parsedBody === "string" && parsedBody.length > 0
  48. )
  49. )
  50. )
  51. )
  52. const getContentTypeFromRawContentType = (rawContentType: string) =>
  53. pipe(
  54. rawContentType,
  55. O.fromPredicate((rct) => rct.length > 0),
  56. // get everything before semi-colon
  57. O.map(flow(S.toLowerCase, S.split(";"), RNEA.head)),
  58. // if rawContentType is valid, cast it to contentType type
  59. O.filter((ct) => Object.keys(knownContentTypes).includes(ct)),
  60. O.map((ct) => ct as HoppRESTReqBody["contentType"])
  61. )
  62. const getContentTypeFromRawData = (rawData: string) =>
  63. pipe(
  64. rawData,
  65. O.fromPredicate((rd) => rd.length > 0),
  66. O.map(detectContentType)
  67. )
  68. export const getBody = (
  69. rawData: string,
  70. rawContentType: string,
  71. contentType: HoppRESTReqBody["contentType"]
  72. ): O.Option<BodyReturnType> => {
  73. return pipe(
  74. O.Do,
  75. O.bind("cType", () =>
  76. pipe(
  77. // get provided content-type
  78. contentType,
  79. O.fromNullable,
  80. // or figure it out
  81. O.alt(() => getContentTypeFromRawContentType(rawContentType)),
  82. O.alt(() => getContentTypeFromRawData(rawData))
  83. )
  84. ),
  85. O.bind("rData", () =>
  86. pipe(
  87. rawData,
  88. O.fromPredicate(() => rawData.length > 0)
  89. )
  90. ),
  91. O.bind("ctBody", ({ cType, rData }) =>
  92. pipe(rawContentType, getBodyFromContentType(rData, cType))
  93. ),
  94. O.map(({ cType, ctBody }) =>
  95. typeof ctBody === "string"
  96. ? {
  97. type: "NON_FORMDATA",
  98. body: {
  99. body: ctBody,
  100. contentType: cType as Exclude<
  101. ValidContentTypes,
  102. "multipart/form-data"
  103. >,
  104. },
  105. }
  106. : { type: "FORMDATA", body: ctBody }
  107. )
  108. )
  109. }
  110. /**
  111. * Parses and structures multipart/form-data from -F argument of curl command
  112. * @param parsedArguments Parsed Arguments object
  113. * @returns Option of Record<string, string> type containing key-value pairs of multipart/form-data
  114. */
  115. export function getFArgumentMultipartData(
  116. parsedArguments: parser.Arguments
  117. ): O.Option<Record<string, string>> {
  118. // --form or -F multipart data
  119. return pipe(
  120. parsedArguments,
  121. // make it an array if not already
  122. O.fromPredicate(objHasProperty("F", "string")),
  123. O.map((args) => [args.F]),
  124. O.alt(() =>
  125. pipe(
  126. parsedArguments,
  127. O.fromPredicate(objHasArrayProperty("F", "string")),
  128. O.map((args) => args.F)
  129. )
  130. ),
  131. O.chain(
  132. flow(
  133. A.map(S.split("=")),
  134. // can only have a key and no value
  135. O.fromPredicate((fArgs) => fArgs.length > 0),
  136. O.map(
  137. flow(
  138. A.map(([k, v]) =>
  139. pipe(
  140. parsedArguments,
  141. // form-string option allows for "@" and "<" prefixes
  142. // without them being considered as files
  143. O.fromPredicate(objHasProperty("form-string", "boolean")),
  144. O.match(
  145. // leave the value field empty for files
  146. () => [k, v[0] === "@" || v[0] === "<" ? "" : v],
  147. (_) => [k, v]
  148. )
  149. )
  150. ),
  151. A.map(([k, v]) => [k, v] as [string, string]),
  152. tupleToRecord
  153. )
  154. )
  155. )
  156. )
  157. )
  158. }