RESTSession.ts 17 KB

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