RESTSession.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
  2. import { Ref } from "@nuxtjs/composition-api"
  3. import {
  4. FormDataKeyValue,
  5. HoppRESTHeader,
  6. HoppRESTParam,
  7. HoppRESTReqBody,
  8. HoppRESTRequest,
  9. RESTReqSchemaVersion,
  10. HoppRESTAuth,
  11. ValidContentTypes,
  12. } from "@hoppscotch/data"
  13. import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
  14. import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
  15. import { useStream } from "~/helpers/utils/composables"
  16. import { HoppTestResult } from "~/helpers/types/HoppTestResult"
  17. import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
  18. import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
  19. type RESTSession = {
  20. request: HoppRESTRequest
  21. response: HoppRESTResponse | null
  22. testResults: HoppTestResult | null
  23. saveContext: HoppRequestSaveContext | null
  24. }
  25. export const getDefaultRESTRequest = (): HoppRESTRequest => ({
  26. v: RESTReqSchemaVersion,
  27. endpoint: "https://echo.hoppscotch.io",
  28. name: "Untitled request",
  29. params: [],
  30. headers: [],
  31. method: "GET",
  32. auth: {
  33. authType: "none",
  34. authActive: true,
  35. },
  36. preRequestScript: "",
  37. testScript: "",
  38. body: {
  39. contentType: null,
  40. body: null,
  41. },
  42. })
  43. const defaultRESTSession: RESTSession = {
  44. request: getDefaultRESTRequest(),
  45. response: null,
  46. testResults: null,
  47. saveContext: null,
  48. }
  49. const dispatchers = defineDispatchers({
  50. setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
  51. return {
  52. request: req,
  53. }
  54. },
  55. setRequestName(curr: RESTSession, { newName }: { newName: string }) {
  56. return {
  57. request: {
  58. ...curr.request,
  59. name: newName,
  60. },
  61. }
  62. },
  63. setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
  64. return {
  65. request: {
  66. ...curr.request,
  67. endpoint: newEndpoint,
  68. },
  69. }
  70. },
  71. setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
  72. return {
  73. request: {
  74. ...curr.request,
  75. params: entries,
  76. },
  77. }
  78. },
  79. addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
  80. return {
  81. request: {
  82. ...curr.request,
  83. params: [...curr.request.params, newParam],
  84. },
  85. }
  86. },
  87. updateParam(
  88. curr: RESTSession,
  89. { index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
  90. ) {
  91. const newParams = curr.request.params.map((param, i) => {
  92. if (i === index) return updatedParam
  93. else return param
  94. })
  95. return {
  96. request: {
  97. ...curr.request,
  98. params: newParams,
  99. },
  100. }
  101. },
  102. deleteParam(curr: RESTSession, { index }: { index: number }) {
  103. const newParams = curr.request.params.filter((_x, i) => i !== index)
  104. return {
  105. request: {
  106. ...curr.request,
  107. params: newParams,
  108. },
  109. }
  110. },
  111. deleteAllParams(curr: RESTSession) {
  112. return {
  113. request: {
  114. ...curr.request,
  115. params: [],
  116. },
  117. }
  118. },
  119. updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
  120. return {
  121. request: {
  122. ...curr.request,
  123. method: newMethod,
  124. },
  125. }
  126. },
  127. setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
  128. return {
  129. request: {
  130. ...curr.request,
  131. headers: entries,
  132. },
  133. }
  134. },
  135. addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
  136. return {
  137. request: {
  138. ...curr.request,
  139. headers: [...curr.request.headers, entry],
  140. },
  141. }
  142. },
  143. updateHeader(
  144. curr: RESTSession,
  145. { index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
  146. ) {
  147. return {
  148. request: {
  149. ...curr.request,
  150. headers: curr.request.headers.map((header, i) => {
  151. if (i === index) return updatedEntry
  152. else return header
  153. }),
  154. },
  155. }
  156. },
  157. deleteHeader(curr: RESTSession, { index }: { index: number }) {
  158. return {
  159. request: {
  160. ...curr.request,
  161. headers: curr.request.headers.filter((_, i) => i !== index),
  162. },
  163. }
  164. },
  165. deleteAllHeaders(curr: RESTSession) {
  166. return {
  167. request: {
  168. ...curr.request,
  169. headers: [],
  170. },
  171. }
  172. },
  173. setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
  174. return {
  175. request: {
  176. ...curr.request,
  177. auth: newAuth,
  178. },
  179. }
  180. },
  181. setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
  182. return {
  183. request: {
  184. ...curr.request,
  185. preRequestScript: newScript,
  186. },
  187. }
  188. },
  189. setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
  190. return {
  191. request: {
  192. ...curr.request,
  193. testScript: newScript,
  194. },
  195. }
  196. },
  197. setContentType(
  198. curr: RESTSession,
  199. { newContentType }: { newContentType: ValidContentTypes | null }
  200. ) {
  201. // TODO: persist body evenafter switching content typees
  202. return {
  203. request: {
  204. ...curr.request,
  205. body: applyBodyTransition(curr.request.body, newContentType),
  206. },
  207. }
  208. },
  209. addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
  210. // Only perform update if the current content-type is formdata
  211. if (curr.request.body.contentType !== "multipart/form-data") return {}
  212. return {
  213. request: {
  214. ...curr.request,
  215. body: <HoppRESTReqBody>{
  216. contentType: "multipart/form-data",
  217. body: [...curr.request.body.body, entry],
  218. },
  219. },
  220. }
  221. },
  222. deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
  223. // Only perform update if the current content-type is formdata
  224. if (curr.request.body.contentType !== "multipart/form-data") return {}
  225. return {
  226. request: {
  227. ...curr.request,
  228. body: <HoppRESTReqBody>{
  229. contentType: "multipart/form-data",
  230. body: curr.request.body.body.filter((_, i) => i !== index),
  231. },
  232. },
  233. }
  234. },
  235. updateFormDataEntry(
  236. curr: RESTSession,
  237. { index, entry }: { index: number; entry: FormDataKeyValue }
  238. ) {
  239. // Only perform update if the current content-type is formdata
  240. if (curr.request.body.contentType !== "multipart/form-data") return {}
  241. return {
  242. request: {
  243. ...curr.request,
  244. body: <HoppRESTReqBody>{
  245. contentType: "multipart/form-data",
  246. body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
  247. },
  248. },
  249. }
  250. },
  251. deleteAllFormDataEntries(curr: RESTSession) {
  252. // Only perform update if the current content-type is formdata
  253. if (curr.request.body.contentType !== "multipart/form-data") return {}
  254. return {
  255. request: {
  256. ...curr.request,
  257. body: <HoppRESTReqBody>{
  258. contentType: "multipart/form-data",
  259. body: [],
  260. },
  261. },
  262. }
  263. },
  264. setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
  265. return {
  266. request: {
  267. ...curr.request,
  268. body: newBody,
  269. },
  270. }
  271. },
  272. updateResponse(
  273. _curr: RESTSession,
  274. { updatedRes }: { updatedRes: HoppRESTResponse | null }
  275. ) {
  276. return {
  277. response: updatedRes,
  278. }
  279. },
  280. clearResponse(_curr: RESTSession) {
  281. return {
  282. response: null,
  283. }
  284. },
  285. setTestResults(
  286. _curr: RESTSession,
  287. { newResults }: { newResults: HoppTestResult | null }
  288. ) {
  289. return {
  290. testResults: newResults,
  291. }
  292. },
  293. setSaveContext(
  294. _,
  295. { newContext }: { newContext: HoppRequestSaveContext | null }
  296. ) {
  297. return {
  298. saveContext: newContext,
  299. }
  300. },
  301. })
  302. const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
  303. export function getRESTRequest() {
  304. return restSessionStore.subject$.value.request
  305. }
  306. export function setRESTRequest(
  307. req: HoppRESTRequest,
  308. saveContext?: HoppRequestSaveContext | null
  309. ) {
  310. restSessionStore.dispatch({
  311. dispatcher: "setRequest",
  312. payload: {
  313. req,
  314. },
  315. })
  316. if (saveContext) setRESTSaveContext(saveContext)
  317. }
  318. export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
  319. restSessionStore.dispatch({
  320. dispatcher: "setSaveContext",
  321. payload: {
  322. newContext: saveContext,
  323. },
  324. })
  325. }
  326. export function getRESTSaveContext() {
  327. return restSessionStore.value.saveContext
  328. }
  329. export function resetRESTRequest() {
  330. setRESTRequest(getDefaultRESTRequest())
  331. }
  332. export function setRESTEndpoint(newEndpoint: string) {
  333. restSessionStore.dispatch({
  334. dispatcher: "setEndpoint",
  335. payload: {
  336. newEndpoint,
  337. },
  338. })
  339. }
  340. export function setRESTRequestName(newName: string) {
  341. restSessionStore.dispatch({
  342. dispatcher: "setRequestName",
  343. payload: {
  344. newName,
  345. },
  346. })
  347. }
  348. export function setRESTParams(entries: HoppRESTParam[]) {
  349. restSessionStore.dispatch({
  350. dispatcher: "setParams",
  351. payload: {
  352. entries,
  353. },
  354. })
  355. }
  356. export function addRESTParam(newParam: HoppRESTParam) {
  357. restSessionStore.dispatch({
  358. dispatcher: "addParam",
  359. payload: {
  360. newParam,
  361. },
  362. })
  363. }
  364. export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
  365. restSessionStore.dispatch({
  366. dispatcher: "updateParam",
  367. payload: {
  368. updatedParam,
  369. index,
  370. },
  371. })
  372. }
  373. export function deleteRESTParam(index: number) {
  374. restSessionStore.dispatch({
  375. dispatcher: "deleteParam",
  376. payload: {
  377. index,
  378. },
  379. })
  380. }
  381. export function deleteAllRESTParams() {
  382. restSessionStore.dispatch({
  383. dispatcher: "deleteAllParams",
  384. payload: {},
  385. })
  386. }
  387. export function updateRESTMethod(newMethod: string) {
  388. restSessionStore.dispatch({
  389. dispatcher: "updateMethod",
  390. payload: {
  391. newMethod,
  392. },
  393. })
  394. }
  395. export function setRESTHeaders(entries: HoppRESTHeader[]) {
  396. restSessionStore.dispatch({
  397. dispatcher: "setHeaders",
  398. payload: {
  399. entries,
  400. },
  401. })
  402. }
  403. export function addRESTHeader(entry: HoppRESTHeader) {
  404. restSessionStore.dispatch({
  405. dispatcher: "addHeader",
  406. payload: {
  407. entry,
  408. },
  409. })
  410. }
  411. export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
  412. restSessionStore.dispatch({
  413. dispatcher: "updateHeader",
  414. payload: {
  415. index,
  416. updatedEntry,
  417. },
  418. })
  419. }
  420. export function deleteRESTHeader(index: number) {
  421. restSessionStore.dispatch({
  422. dispatcher: "deleteHeader",
  423. payload: {
  424. index,
  425. },
  426. })
  427. }
  428. export function deleteAllRESTHeaders() {
  429. restSessionStore.dispatch({
  430. dispatcher: "deleteAllHeaders",
  431. payload: {},
  432. })
  433. }
  434. export function setRESTAuth(newAuth: HoppRESTAuth) {
  435. restSessionStore.dispatch({
  436. dispatcher: "setAuth",
  437. payload: {
  438. newAuth,
  439. },
  440. })
  441. }
  442. export function setRESTPreRequestScript(newScript: string) {
  443. restSessionStore.dispatch({
  444. dispatcher: "setPreRequestScript",
  445. payload: {
  446. newScript,
  447. },
  448. })
  449. }
  450. export function setRESTTestScript(newScript: string) {
  451. restSessionStore.dispatch({
  452. dispatcher: "setTestScript",
  453. payload: {
  454. newScript,
  455. },
  456. })
  457. }
  458. export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
  459. restSessionStore.dispatch({
  460. dispatcher: "setRequestBody",
  461. payload: {
  462. newBody,
  463. },
  464. })
  465. }
  466. export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
  467. restSessionStore.dispatch({
  468. dispatcher: "updateResponse",
  469. payload: {
  470. updatedRes,
  471. },
  472. })
  473. }
  474. export function clearRESTResponse() {
  475. restSessionStore.dispatch({
  476. dispatcher: "clearResponse",
  477. payload: {},
  478. })
  479. }
  480. export function setRESTTestResults(newResults: HoppTestResult | null) {
  481. restSessionStore.dispatch({
  482. dispatcher: "setTestResults",
  483. payload: {
  484. newResults,
  485. },
  486. })
  487. }
  488. export function addFormDataEntry(entry: FormDataKeyValue) {
  489. restSessionStore.dispatch({
  490. dispatcher: "addFormDataEntry",
  491. payload: {
  492. entry,
  493. },
  494. })
  495. }
  496. export function deleteFormDataEntry(index: number) {
  497. restSessionStore.dispatch({
  498. dispatcher: "deleteFormDataEntry",
  499. payload: {
  500. index,
  501. },
  502. })
  503. }
  504. export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
  505. restSessionStore.dispatch({
  506. dispatcher: "updateFormDataEntry",
  507. payload: {
  508. index,
  509. entry,
  510. },
  511. })
  512. }
  513. export function setRESTContentType(newContentType: ValidContentTypes | null) {
  514. restSessionStore.dispatch({
  515. dispatcher: "setContentType",
  516. payload: {
  517. newContentType,
  518. },
  519. })
  520. }
  521. export function deleteAllFormDataEntries() {
  522. restSessionStore.dispatch({
  523. dispatcher: "deleteAllFormDataEntries",
  524. payload: {},
  525. })
  526. }
  527. export const restSaveContext$ = restSessionStore.subject$.pipe(
  528. pluck("saveContext"),
  529. distinctUntilChanged()
  530. )
  531. export const restRequest$ = restSessionStore.subject$.pipe(
  532. pluck("request"),
  533. distinctUntilChanged()
  534. )
  535. export const restRequestName$ = restRequest$.pipe(
  536. pluck("name"),
  537. distinctUntilChanged()
  538. )
  539. export const restEndpoint$ = restSessionStore.subject$.pipe(
  540. pluck("request", "endpoint"),
  541. distinctUntilChanged()
  542. )
  543. export const restParams$ = restSessionStore.subject$.pipe(
  544. pluck("request", "params"),
  545. distinctUntilChanged()
  546. )
  547. export const restActiveParamsCount$ = restParams$.pipe(
  548. map(
  549. (params) =>
  550. params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
  551. )
  552. )
  553. export const restMethod$ = restSessionStore.subject$.pipe(
  554. pluck("request", "method"),
  555. distinctUntilChanged()
  556. )
  557. export const restHeaders$ = restSessionStore.subject$.pipe(
  558. pluck("request", "headers"),
  559. distinctUntilChanged()
  560. )
  561. export const restActiveHeadersCount$ = restHeaders$.pipe(
  562. map(
  563. (params) =>
  564. params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
  565. )
  566. )
  567. export const restAuth$ = restRequest$.pipe(pluck("auth"))
  568. export const restPreRequestScript$ = restSessionStore.subject$.pipe(
  569. pluck("request", "preRequestScript"),
  570. distinctUntilChanged()
  571. )
  572. export const restContentType$ = restRequest$.pipe(
  573. pluck("body", "contentType"),
  574. distinctUntilChanged()
  575. )
  576. export const restTestScript$ = restSessionStore.subject$.pipe(
  577. pluck("request", "testScript"),
  578. distinctUntilChanged()
  579. )
  580. export const restReqBody$ = restSessionStore.subject$.pipe(
  581. pluck("request", "body"),
  582. distinctUntilChanged()
  583. )
  584. export const restResponse$ = restSessionStore.subject$.pipe(
  585. pluck("response"),
  586. distinctUntilChanged()
  587. )
  588. export const completedRESTResponse$ = restResponse$.pipe(
  589. filter(
  590. (res) =>
  591. res !== null &&
  592. res.type !== "loading" &&
  593. res.type !== "network_fail" &&
  594. res.type !== "script_fail"
  595. )
  596. )
  597. export const restTestResults$ = restSessionStore.subject$.pipe(
  598. pluck("testResults"),
  599. distinctUntilChanged()
  600. )
  601. /**
  602. * A Vue 3 composable function that gives access to a ref
  603. * which is updated to the preRequestScript value in the store.
  604. * The ref value is kept in sync with the store and all writes
  605. * to the ref are dispatched to the store as `setPreRequestScript`
  606. * dispatches.
  607. */
  608. export function usePreRequestScript(): Ref<string> {
  609. return useStream(
  610. restPreRequestScript$,
  611. restSessionStore.value.request.preRequestScript,
  612. (value) => {
  613. setRESTPreRequestScript(value)
  614. }
  615. )
  616. }
  617. /**
  618. * A Vue 3 composable function that gives access to a ref
  619. * which is updated to the testScript value in the store.
  620. * The ref value is kept in sync with the store and all writes
  621. * to the ref are dispatched to the store as `setTestScript`
  622. * dispatches.
  623. */
  624. export function useTestScript(): Ref<string> {
  625. return useStream(
  626. restTestScript$,
  627. restSessionStore.value.request.testScript,
  628. (value) => {
  629. setRESTTestScript(value)
  630. }
  631. )
  632. }
  633. export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
  634. return useStream(
  635. restReqBody$,
  636. restSessionStore.value.request.body,
  637. setRESTReqBody
  638. )
  639. }
  640. export function useRESTRequestName(): Ref<string> {
  641. return useStream(
  642. restRequestName$,
  643. restSessionStore.value.request.name,
  644. setRESTRequestName
  645. )
  646. }