insomnia.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { convert, ImportRequest } from "insomnia-importers"
  2. import { pipe, flow } from "fp-ts/function"
  3. import {
  4. HoppRESTAuth,
  5. HoppRESTHeader,
  6. HoppRESTParam,
  7. HoppRESTReqBody,
  8. HoppRESTRequest,
  9. knownContentTypes,
  10. makeRESTRequest,
  11. HoppCollection,
  12. makeCollection,
  13. } from "@hoppscotch/data"
  14. import * as A from "fp-ts/Array"
  15. import * as S from "fp-ts/string"
  16. import * as TO from "fp-ts/TaskOption"
  17. import * as TE from "fp-ts/TaskEither"
  18. import { step } from "../steps"
  19. import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
  20. // TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
  21. type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Y>
  22. ? Y
  23. : never
  24. type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
  25. type InsomniaResource = ImportRequest
  26. type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
  27. type InsomniaRequestResource = ImportRequest & { _type: "request" }
  28. const parseInsomniaDoc = (content: string) =>
  29. TO.tryCatch(() => convert(content))
  30. const replaceVarTemplating = flow(
  31. S.replace(/{{\s*/g, "<<"),
  32. S.replace(/\s*}}/g, ">>")
  33. )
  34. const getFoldersIn = (
  35. folder: InsomniaFolderResource | null,
  36. resources: InsomniaResource[]
  37. ) =>
  38. pipe(
  39. resources,
  40. A.filter(
  41. (x): x is InsomniaFolderResource =>
  42. (x._type === "request_group" || x._type === "workspace") &&
  43. x.parentId === (folder?._id ?? null)
  44. )
  45. )
  46. const getRequestsIn = (
  47. folder: InsomniaFolderResource | null,
  48. resources: InsomniaResource[]
  49. ) =>
  50. pipe(
  51. resources,
  52. A.filter(
  53. (x): x is InsomniaRequestResource =>
  54. x._type === "request" && x.parentId === (folder?._id ?? null)
  55. )
  56. )
  57. /**
  58. * The provided type by insomnia-importers, this type corrects it
  59. */
  60. type InsoReqAuth =
  61. | { type: "basic"; disabled?: boolean; username?: string; password?: string }
  62. | {
  63. type: "oauth2"
  64. disabled?: boolean
  65. accessTokenUrl?: string
  66. authorizationUrl?: string
  67. clientId?: string
  68. scope?: string
  69. }
  70. | {
  71. type: "bearer"
  72. disabled?: boolean
  73. token?: string
  74. }
  75. const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
  76. if (!req.authentication) return { authType: "none", authActive: true }
  77. const auth = req.authentication as InsoReqAuth
  78. if (auth.type === "basic")
  79. return {
  80. authType: "basic",
  81. authActive: true,
  82. username: replaceVarTemplating(auth.username ?? ""),
  83. password: replaceVarTemplating(auth.password ?? ""),
  84. }
  85. else if (auth.type === "oauth2")
  86. return {
  87. authType: "oauth-2",
  88. authActive: !(auth.disabled ?? false),
  89. accessTokenURL: replaceVarTemplating(auth.accessTokenUrl ?? ""),
  90. authURL: replaceVarTemplating(auth.authorizationUrl ?? ""),
  91. clientID: replaceVarTemplating(auth.clientId ?? ""),
  92. oidcDiscoveryURL: "",
  93. scope: replaceVarTemplating(auth.scope ?? ""),
  94. token: "",
  95. }
  96. else if (auth.type === "bearer")
  97. return {
  98. authType: "bearer",
  99. authActive: true,
  100. token: replaceVarTemplating(auth.token ?? ""),
  101. }
  102. return { authType: "none", authActive: true }
  103. }
  104. const getHoppReqBody = (req: InsomniaRequestResource): HoppRESTReqBody => {
  105. if (!req.body) return { contentType: null, body: null }
  106. if (typeof req.body === "string") {
  107. const contentType =
  108. req.headers?.find(
  109. (header) => header.name.toLowerCase() === "content-type"
  110. )?.value ?? "text/plain"
  111. return { contentType, body: replaceVarTemplating(req.body) }
  112. }
  113. if (req.body.mimeType === "multipart/form-data") {
  114. return {
  115. contentType: "multipart/form-data",
  116. body:
  117. req.body.params?.map((param) => ({
  118. key: replaceVarTemplating(param.name),
  119. value: replaceVarTemplating(param.value ?? ""),
  120. active: !(param.disabled ?? false),
  121. isFile: false,
  122. })) ?? [],
  123. }
  124. } else if (req.body.mimeType === "application/x-www-form-urlencoded") {
  125. return {
  126. contentType: "application/x-www-form-urlencoded",
  127. body:
  128. req.body.params
  129. ?.filter((param) => !(param.disabled ?? false))
  130. .map(
  131. (param) =>
  132. `${replaceVarTemplating(param.name)}: ${replaceVarTemplating(
  133. param.value ?? ""
  134. )}`
  135. )
  136. .join("\n") ?? "",
  137. }
  138. } else if (
  139. Object.keys(knownContentTypes).includes(req.body.mimeType ?? "text/plain")
  140. ) {
  141. return {
  142. contentType: (req.body.mimeType ?? "text/plain") as any,
  143. body: replaceVarTemplating(req.body.text ?? "") as any,
  144. }
  145. }
  146. return { contentType: null, body: null }
  147. }
  148. const getHoppReqHeaders = (req: InsomniaRequestResource): HoppRESTHeader[] =>
  149. req.headers?.map((header) => ({
  150. key: replaceVarTemplating(header.name),
  151. value: replaceVarTemplating(header.value),
  152. active: !header.disabled,
  153. })) ?? []
  154. const getHoppReqParams = (req: InsomniaRequestResource): HoppRESTParam[] =>
  155. req.parameters?.map((param) => ({
  156. key: replaceVarTemplating(param.name),
  157. value: replaceVarTemplating(param.value ?? ""),
  158. active: !(param.disabled ?? false),
  159. })) ?? []
  160. const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
  161. makeRESTRequest({
  162. name: req.name ?? "Untitled Request",
  163. method: req.method?.toUpperCase() ?? "GET",
  164. endpoint: replaceVarTemplating(req.url ?? ""),
  165. auth: getHoppReqAuth(req),
  166. body: getHoppReqBody(req),
  167. headers: getHoppReqHeaders(req),
  168. params: getHoppReqParams(req),
  169. preRequestScript: "",
  170. testScript: "",
  171. })
  172. const getHoppFolder = (
  173. folderRes: InsomniaFolderResource,
  174. resources: InsomniaResource[]
  175. ): HoppCollection<HoppRESTRequest> =>
  176. makeCollection({
  177. name: folderRes.name ?? "",
  178. folders: getFoldersIn(folderRes, resources).map((f) =>
  179. getHoppFolder(f, resources)
  180. ),
  181. requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
  182. })
  183. const getHoppCollections = (doc: InsomniaDoc) =>
  184. getFoldersIn(null, doc.data.resources).map((f) =>
  185. getHoppFolder(f, doc.data.resources)
  186. )
  187. export default defineImporter({
  188. id: "insomnia",
  189. name: "import.from_insomnia",
  190. applicableTo: ["my-collections", "team-collections", "url-import"],
  191. icon: "insomnia",
  192. steps: [
  193. step({
  194. stepName: "FILE_IMPORT",
  195. metadata: {
  196. caption: "import.from_insomnia_description",
  197. acceptedFileTypes: ".json, .yaml",
  198. },
  199. }),
  200. ] as const,
  201. importer: ([fileContent]) =>
  202. pipe(
  203. fileContent,
  204. parseInsomniaDoc,
  205. TO.map(getHoppCollections),
  206. TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
  207. ),
  208. })