postman.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import {
  2. Collection as PMCollection,
  3. Item,
  4. ItemGroup,
  5. QueryParam,
  6. RequestAuthDefinition,
  7. VariableDefinition,
  8. } from "postman-collection"
  9. import {
  10. HoppRESTAuth,
  11. HoppRESTHeader,
  12. HoppRESTParam,
  13. HoppRESTReqBody,
  14. HoppRESTRequest,
  15. makeRESTRequest,
  16. HoppCollection,
  17. makeCollection,
  18. ValidContentTypes,
  19. knownContentTypes,
  20. FormDataKeyValue,
  21. } from "@hoppscotch/data"
  22. import { pipe, flow } from "fp-ts/function"
  23. import * as S from "fp-ts/string"
  24. import * as A from "fp-ts/Array"
  25. import * as O from "fp-ts/Option"
  26. import * as TE from "fp-ts/TaskEither"
  27. import { step } from "../steps"
  28. import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
  29. import { PMRawLanguage } from "~/types/pm-coll-exts"
  30. import { stringArrayJoin } from "~/helpers/functional/array"
  31. const safeParseJSON = (jsonStr: string) => O.tryCatch(() => JSON.parse(jsonStr))
  32. const isPMItem = (x: unknown): x is Item => Item.isItem(x)
  33. const replacePMVarTemplating = flow(
  34. S.replace(/{{\s*/g, "<<"),
  35. S.replace(/\s*}}/g, ">>")
  36. )
  37. const PMRawLanguageOptionsToContentTypeMap: Record<
  38. PMRawLanguage,
  39. ValidContentTypes
  40. > = {
  41. text: "text/plain",
  42. javascript: "text/plain",
  43. json: "application/json",
  44. html: "text/html",
  45. xml: "application/xml",
  46. }
  47. const isPMItemGroup = (x: unknown): x is ItemGroup<Item> =>
  48. ItemGroup.isItemGroup(x)
  49. const readPMCollection = (def: string) =>
  50. pipe(
  51. def,
  52. safeParseJSON,
  53. O.chain((data) => O.tryCatch(() => new PMCollection(data)))
  54. )
  55. const getHoppReqHeaders = (item: Item): HoppRESTHeader[] =>
  56. pipe(
  57. item.request.headers.all(),
  58. A.map((header) => {
  59. return <HoppRESTHeader>{
  60. key: replacePMVarTemplating(header.key),
  61. value: replacePMVarTemplating(header.value),
  62. active: !header.disabled,
  63. }
  64. })
  65. )
  66. const getHoppReqParams = (item: Item): HoppRESTParam[] => {
  67. return pipe(
  68. item.request.url.query.all(),
  69. A.filter(
  70. (param): param is QueryParam & { key: string } =>
  71. param.key !== undefined && param.key !== null && param.key.length > 0
  72. ),
  73. A.map((param) => {
  74. return <HoppRESTHeader>{
  75. key: replacePMVarTemplating(param.key),
  76. value: replacePMVarTemplating(param.value ?? ""),
  77. active: !param.disabled,
  78. }
  79. })
  80. )
  81. }
  82. type PMRequestAuthDef<
  83. AuthType extends RequestAuthDefinition["type"] = RequestAuthDefinition["type"]
  84. > = AuthType extends RequestAuthDefinition["type"] & string
  85. ? // eslint-disable-next-line no-unused-vars
  86. { type: AuthType } & { [x in AuthType]: VariableDefinition[] }
  87. : { type: AuthType }
  88. const getVariableValue = (defs: VariableDefinition[], key: string) =>
  89. defs.find((param) => param.key === key)?.value as string | undefined
  90. const getHoppReqAuth = (item: Item): HoppRESTAuth => {
  91. if (!item.request.auth) return { authType: "none", authActive: true }
  92. // Cast to the type for more stricter checking down the line
  93. const auth = item.request.auth as unknown as PMRequestAuthDef
  94. if (auth.type === "basic") {
  95. return {
  96. authType: "basic",
  97. authActive: true,
  98. username: replacePMVarTemplating(
  99. getVariableValue(auth.basic, "username") ?? ""
  100. ),
  101. password: replacePMVarTemplating(
  102. getVariableValue(auth.basic, "password") ?? ""
  103. ),
  104. }
  105. } else if (auth.type === "apikey") {
  106. return {
  107. authType: "api-key",
  108. authActive: true,
  109. key: replacePMVarTemplating(getVariableValue(auth.apikey, "key") ?? ""),
  110. value: replacePMVarTemplating(
  111. getVariableValue(auth.apikey, "value") ?? ""
  112. ),
  113. addTo:
  114. (getVariableValue(auth.apikey, "in") ?? "query") === "query"
  115. ? "Query params"
  116. : "Headers",
  117. }
  118. } else if (auth.type === "bearer") {
  119. return {
  120. authType: "bearer",
  121. authActive: true,
  122. token: replacePMVarTemplating(
  123. getVariableValue(auth.bearer, "token") ?? ""
  124. ),
  125. }
  126. } else if (auth.type === "oauth2") {
  127. return {
  128. authType: "oauth-2",
  129. authActive: true,
  130. accessTokenURL: replacePMVarTemplating(
  131. getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
  132. ),
  133. authURL: replacePMVarTemplating(
  134. getVariableValue(auth.oauth2, "authUrl") ?? ""
  135. ),
  136. clientID: replacePMVarTemplating(
  137. getVariableValue(auth.oauth2, "clientId") ?? ""
  138. ),
  139. scope: replacePMVarTemplating(
  140. getVariableValue(auth.oauth2, "scope") ?? ""
  141. ),
  142. token: replacePMVarTemplating(
  143. getVariableValue(auth.oauth2, "accessToken") ?? ""
  144. ),
  145. oidcDiscoveryURL: "",
  146. }
  147. }
  148. return { authType: "none", authActive: true }
  149. }
  150. const getHoppReqBody = (item: Item): HoppRESTReqBody => {
  151. if (!item.request.body) return { contentType: null, body: null }
  152. const body = item.request.body
  153. if (body.mode === "formdata") {
  154. return {
  155. contentType: "multipart/form-data",
  156. body: pipe(
  157. body.formdata?.all() ?? [],
  158. A.map(
  159. (param) =>
  160. <FormDataKeyValue>{
  161. key: replacePMVarTemplating(param.key),
  162. value: replacePMVarTemplating(
  163. param.type === "text" ? (param.value as string) : ""
  164. ),
  165. active: !param.disabled,
  166. isFile: false, // TODO: Preserve isFile state ?
  167. }
  168. )
  169. ),
  170. }
  171. } else if (body.mode === "urlencoded") {
  172. return {
  173. contentType: "application/x-www-form-urlencoded",
  174. body: pipe(
  175. body.urlencoded?.all() ?? [],
  176. A.map(
  177. (param) =>
  178. `${replacePMVarTemplating(
  179. param.key ?? ""
  180. )}: ${replacePMVarTemplating(param.value ?? "")}`
  181. ),
  182. stringArrayJoin("\n")
  183. ),
  184. }
  185. } else if (body.mode === "raw") {
  186. return pipe(
  187. O.Do,
  188. // Extract content-type
  189. O.bind("contentType", () =>
  190. pipe(
  191. // Get the info from the content-type header
  192. getHoppReqHeaders(item),
  193. A.findFirst(({ key }) => key.toLowerCase() === "content-type"),
  194. O.map((x) => x.value),
  195. // Make sure it is a content-type Hopp can work with
  196. O.filter(
  197. (contentType): contentType is ValidContentTypes =>
  198. contentType in knownContentTypes
  199. ),
  200. // Back-up plan, assume language from raw language defintion
  201. O.alt(() =>
  202. pipe(
  203. body.options?.raw?.language,
  204. O.fromNullable,
  205. O.map((lang) => PMRawLanguageOptionsToContentTypeMap[lang])
  206. )
  207. ),
  208. // If that too failed, just assume "text/plain"
  209. O.getOrElse((): ValidContentTypes => "text/plain"),
  210. O.of
  211. )
  212. ),
  213. // Extract and parse body
  214. O.bind("body", () =>
  215. pipe(body.raw, O.fromNullable, O.map(replacePMVarTemplating))
  216. ),
  217. // Return null content-type if failed, else return parsed
  218. O.match(
  219. () =>
  220. <HoppRESTReqBody>{
  221. contentType: null,
  222. body: null,
  223. },
  224. ({ contentType, body }) =>
  225. <HoppRESTReqBody>{
  226. contentType,
  227. body,
  228. }
  229. )
  230. )
  231. }
  232. // TODO: File
  233. // TODO: GraphQL ?
  234. return { contentType: null, body: null }
  235. }
  236. const getHoppReqURL = (item: Item): string =>
  237. pipe(
  238. item.request.url.toString(false),
  239. S.replace(/\?.+/g, ""),
  240. replacePMVarTemplating
  241. )
  242. const getHoppRequest = (item: Item): HoppRESTRequest => {
  243. return makeRESTRequest({
  244. name: item.name,
  245. endpoint: getHoppReqURL(item),
  246. method: item.request.method.toUpperCase(),
  247. headers: getHoppReqHeaders(item),
  248. params: getHoppReqParams(item),
  249. auth: getHoppReqAuth(item),
  250. body: getHoppReqBody(item),
  251. // TODO: Decide about this
  252. preRequestScript: "",
  253. testScript: "",
  254. })
  255. }
  256. const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
  257. makeCollection({
  258. name: ig.name,
  259. folders: pipe(
  260. ig.items.all(),
  261. A.filter(isPMItemGroup),
  262. A.map(getHoppFolder)
  263. ),
  264. requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
  265. })
  266. export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
  267. export default defineImporter({
  268. id: "postman",
  269. name: "import.from_postman",
  270. applicableTo: ["my-collections", "team-collections", "url-import"],
  271. icon: "postman",
  272. steps: [
  273. step({
  274. stepName: "FILE_IMPORT",
  275. metadata: {
  276. caption: "import.from_postman_description",
  277. acceptedFileTypes: ".json",
  278. },
  279. }),
  280. ] as const,
  281. importer: ([fileContent]) =>
  282. pipe(
  283. // Try reading
  284. fileContent,
  285. readPMCollection,
  286. O.map(flow(getHoppCollection, A.of)),
  287. TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
  288. ),
  289. })