personal.workspace.ts 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. import {
  2. HoppCollection,
  3. HoppRESTAuth,
  4. HoppRESTHeaders,
  5. makeCollection,
  6. } from "@hoppscotch/data"
  7. import { Service } from "dioc"
  8. import * as E from "fp-ts/Either"
  9. import {
  10. Ref,
  11. computed,
  12. effectScope,
  13. markRaw,
  14. nextTick,
  15. ref,
  16. shallowRef,
  17. watch,
  18. } from "vue"
  19. import PersonalWorkspaceSelector from "~/components/workspace/PersonalWorkspaceSelector.vue"
  20. import { useStreamStatic } from "~/composables/stream"
  21. import {
  22. addRESTCollection,
  23. addRESTFolder,
  24. appendRESTCollections,
  25. editRESTCollection,
  26. editRESTFolder,
  27. editRESTRequest,
  28. moveRESTFolder,
  29. moveRESTRequest,
  30. navigateToFolderWithIndexPath,
  31. removeRESTCollection,
  32. removeRESTFolder,
  33. removeRESTRequest,
  34. restCollectionStore,
  35. saveRESTRequestAs,
  36. updateRESTCollectionOrder,
  37. updateRESTRequestOrder,
  38. } from "~/newstore/collections"
  39. import { platform } from "~/platform"
  40. import { HandleRef, WritableHandleRef } from "~/services/new-workspace/handle"
  41. import { WorkspaceProvider } from "~/services/new-workspace/provider"
  42. import {
  43. RESTCollectionChildrenView,
  44. RESTCollectionJSONView,
  45. RESTCollectionLevelAuthHeadersView,
  46. RESTCollectionViewItem,
  47. RESTSearchResultsView,
  48. RootRESTCollectionView,
  49. } from "~/services/new-workspace/view"
  50. import {
  51. Workspace,
  52. WorkspaceCollection,
  53. WorkspaceDecor,
  54. WorkspaceRequest,
  55. } from "~/services/new-workspace/workspace"
  56. import { HoppRESTRequest } from "@hoppscotch/data"
  57. import { merge } from "lodash-es"
  58. import path from "path"
  59. import { initializeDownloadFile } from "~/helpers/import-export/export"
  60. import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
  61. import IconUser from "~icons/lucide/user"
  62. import { NewWorkspaceService } from ".."
  63. import {
  64. isValidCollectionHandle,
  65. isValidRequestHandle,
  66. isValidWorkspaceHandle,
  67. } from "../helpers"
  68. export class PersonalWorkspaceProviderService
  69. extends Service
  70. implements WorkspaceProvider
  71. {
  72. public static readonly ID = "PERSONAL_WORKSPACE_PROVIDER_SERVICE"
  73. public readonly providerID = "PERSONAL_WORKSPACE_PROVIDER"
  74. private workspaceService = this.bind(NewWorkspaceService)
  75. public workspaceDecor: Ref<WorkspaceDecor> = ref({
  76. headerCurrentIcon: IconUser,
  77. workspaceSelectorComponent: PersonalWorkspaceSelector,
  78. workspaceSelectorPriority: 100,
  79. })
  80. private restCollectionState: Ref<{ state: HoppCollection[] }>
  81. private issuedHandles: WritableHandleRef<
  82. WorkspaceCollection | WorkspaceRequest
  83. >[] = []
  84. public constructor() {
  85. super()
  86. this.restCollectionState = useStreamStatic(
  87. restCollectionStore.subject$,
  88. { state: [] },
  89. () => {
  90. /* noop */
  91. }
  92. )[0]
  93. this.workspaceService.registerWorkspaceProvider(this)
  94. }
  95. /**
  96. * Used to get the index of the request from the path
  97. * @param path The path of the request
  98. * @returns The index of the request
  99. */
  100. private pathToLastIndex(path: string) {
  101. const pathArr = path.split("/")
  102. return parseInt(pathArr[pathArr.length - 1])
  103. }
  104. public createRESTRootCollection(
  105. workspaceHandle: HandleRef<Workspace>,
  106. newCollection: Partial<Exclude<HoppCollection, "id">> & { name: string }
  107. ): Promise<E.Either<unknown, HandleRef<WorkspaceCollection>>> {
  108. if (!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")) {
  109. return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
  110. }
  111. const newCollectionName = newCollection.name
  112. const newCollectionID =
  113. this.restCollectionState.value.state.length.toString()
  114. const newRootCollection = makeCollection({
  115. folders: [],
  116. requests: [],
  117. headers: [],
  118. auth: {
  119. authType: "inherit",
  120. authActive: false,
  121. },
  122. ...newCollection,
  123. })
  124. addRESTCollection(newRootCollection)
  125. platform.analytics?.logEvent({
  126. type: "HOPP_CREATE_COLLECTION",
  127. platform: "rest",
  128. workspaceType: "personal",
  129. isRootCollection: true,
  130. })
  131. return Promise.resolve(
  132. E.right(
  133. computed(() => {
  134. if (
  135. !isValidWorkspaceHandle(
  136. workspaceHandle,
  137. this.providerID,
  138. "personal"
  139. )
  140. ) {
  141. return {
  142. type: "invalid" as const,
  143. reason: "WORKSPACE_INVALIDATED" as const,
  144. }
  145. }
  146. return {
  147. type: "ok",
  148. data: {
  149. providerID: this.providerID,
  150. workspaceID: workspaceHandle.value.data.workspaceID,
  151. collectionID: newCollectionID,
  152. name: newCollectionName,
  153. },
  154. }
  155. })
  156. )
  157. )
  158. }
  159. public createRESTChildCollection(
  160. parentCollectionHandle: HandleRef<WorkspaceCollection>,
  161. newChildCollection: Partial<HoppCollection> & { name: string }
  162. ): Promise<E.Either<unknown, HandleRef<WorkspaceCollection>>> {
  163. if (
  164. !isValidCollectionHandle(
  165. parentCollectionHandle,
  166. this.providerID,
  167. "personal"
  168. )
  169. ) {
  170. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  171. }
  172. const { collectionID, providerID, workspaceID } =
  173. parentCollectionHandle.value.data
  174. const newCollectionName = newChildCollection.name
  175. addRESTFolder(newCollectionName, collectionID)
  176. platform.analytics?.logEvent({
  177. type: "HOPP_CREATE_COLLECTION",
  178. workspaceType: "personal",
  179. isRootCollection: false,
  180. platform: "rest",
  181. })
  182. return Promise.resolve(
  183. E.right(
  184. computed(() => {
  185. if (
  186. !isValidCollectionHandle(
  187. parentCollectionHandle,
  188. this.providerID,
  189. "personal"
  190. )
  191. ) {
  192. return {
  193. type: "invalid" as const,
  194. reason: "COLLECTION_INVALIDATED" as const,
  195. }
  196. }
  197. return {
  198. type: "ok",
  199. data: {
  200. providerID,
  201. workspaceID,
  202. collectionID,
  203. name: newCollectionName,
  204. },
  205. }
  206. })
  207. )
  208. )
  209. }
  210. public updateRESTCollection(
  211. collectionHandle: HandleRef<WorkspaceCollection>,
  212. updatedCollection: Partial<HoppCollection>
  213. ): Promise<E.Either<unknown, void>> {
  214. if (
  215. !isValidCollectionHandle(collectionHandle, this.providerID, "personal")
  216. ) {
  217. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  218. }
  219. const { collectionID } = collectionHandle.value.data
  220. const collection = navigateToFolderWithIndexPath(
  221. this.restCollectionState.value.state,
  222. collectionID.split("/").map((id) => parseInt(id))
  223. )
  224. const newCollection = { ...collection, ...updatedCollection }
  225. const isRootCollection = collectionID.split("/").length === 1
  226. if (isRootCollection) {
  227. editRESTCollection(parseInt(collectionID), newCollection)
  228. } else {
  229. editRESTFolder(collectionID, newCollection)
  230. }
  231. return Promise.resolve(E.right(undefined))
  232. }
  233. public removeRESTCollection(
  234. collectionHandle: HandleRef<WorkspaceCollection>
  235. ): Promise<E.Either<unknown, void>> {
  236. if (
  237. !isValidCollectionHandle(collectionHandle, this.providerID, "personal")
  238. ) {
  239. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  240. }
  241. const { collectionID } = collectionHandle.value.data
  242. const isRootCollection = collectionID.split("/").length === 1
  243. if (isRootCollection) {
  244. const collectionIndex = parseInt(collectionID)
  245. const collectionToRemove = navigateToFolderWithIndexPath(
  246. restCollectionStore.value.state,
  247. [collectionIndex]
  248. )
  249. removeRESTCollection(
  250. collectionIndex,
  251. collectionToRemove ? collectionToRemove.id : undefined
  252. )
  253. } else {
  254. const folderToRemove = path
  255. ? navigateToFolderWithIndexPath(
  256. restCollectionStore.value.state,
  257. collectionID.split("/").map((id) => parseInt(id))
  258. )
  259. : undefined
  260. removeRESTFolder(
  261. collectionID,
  262. folderToRemove ? folderToRemove.id : undefined
  263. )
  264. }
  265. for (const [idx, handle] of this.issuedHandles.entries()) {
  266. if (handle.value.type === "invalid") continue
  267. if ("requestID" in handle.value.data) {
  268. if (handle.value.data.requestID.startsWith(collectionID)) {
  269. // @ts-expect-error - We're deleting the data to invalidate the handle
  270. delete this.issuedHandles[idx].value.data
  271. this.issuedHandles[idx].value.type = "invalid"
  272. // @ts-expect-error - Setting the handle invalidation reason
  273. this.issuedHandles[idx].value.reason = "REQUEST_INVALIDATED"
  274. }
  275. }
  276. }
  277. return Promise.resolve(E.right(undefined))
  278. }
  279. public createRESTRequest(
  280. parentCollectionHandle: HandleRef<WorkspaceCollection>,
  281. newRequest: HoppRESTRequest
  282. ): Promise<E.Either<unknown, HandleRef<WorkspaceRequest>>> {
  283. if (
  284. !isValidCollectionHandle(
  285. parentCollectionHandle,
  286. this.providerID,
  287. "personal"
  288. )
  289. ) {
  290. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  291. }
  292. const { collectionID, providerID, workspaceID } =
  293. parentCollectionHandle.value.data
  294. const insertionIndex = saveRESTRequestAs(collectionID, newRequest)
  295. const requestID = `${collectionID}/${insertionIndex}`
  296. platform.analytics?.logEvent({
  297. type: "HOPP_SAVE_REQUEST",
  298. workspaceType: "personal",
  299. createdNow: true,
  300. platform: "rest",
  301. })
  302. const handle: HandleRef<WorkspaceRequest> = computed(() => {
  303. if (
  304. !isValidCollectionHandle(
  305. parentCollectionHandle,
  306. this.providerID,
  307. "personal"
  308. )
  309. ) {
  310. return {
  311. type: "invalid" as const,
  312. reason: "COLLECTION_INVALIDATED" as const,
  313. }
  314. }
  315. return {
  316. type: "ok",
  317. data: {
  318. providerID,
  319. workspaceID,
  320. collectionID,
  321. requestID,
  322. request: newRequest,
  323. },
  324. }
  325. })
  326. const writableHandle = computed({
  327. get() {
  328. return handle.value
  329. },
  330. set(newValue) {
  331. handle.value = newValue
  332. },
  333. })
  334. this.issuedHandles.push(writableHandle)
  335. return Promise.resolve(E.right(handle))
  336. }
  337. public removeRESTRequest(
  338. requestHandle: HandleRef<WorkspaceRequest>
  339. ): Promise<E.Either<unknown, void>> {
  340. if (!isValidRequestHandle(requestHandle, this.providerID, "personal")) {
  341. return Promise.resolve(E.left("INVALID_REQUEST_HANDLE" as const))
  342. }
  343. const { collectionID, requestID } = requestHandle.value.data
  344. const requestIndex = parseInt(requestID.split("/").slice(-1)[0])
  345. const requestToRemove = navigateToFolderWithIndexPath(
  346. restCollectionStore.value.state,
  347. collectionID.split("/").map((id) => parseInt(id))
  348. )?.requests[requestIndex]
  349. removeRESTRequest(collectionID, requestIndex, requestToRemove?.id)
  350. for (const [idx, handle] of this.issuedHandles.entries()) {
  351. if (handle.value.type === "invalid") continue
  352. if ("requestID" in handle.value.data) {
  353. if (handle.value.data.requestID === requestID) {
  354. // @ts-expect-error - We're deleting the data to invalidate the handle
  355. delete this.issuedHandles[idx].value.data
  356. this.issuedHandles[idx].value.type = "invalid"
  357. // @ts-expect-error - Setting the handle invalidation reason
  358. this.issuedHandles[idx].value.reason = "REQUEST_INVALIDATED"
  359. }
  360. }
  361. }
  362. return Promise.resolve(E.right(undefined))
  363. }
  364. public updateRESTRequest(
  365. requestHandle: HandleRef<WorkspaceRequest>,
  366. updatedRequest: Partial<HoppRESTRequest>
  367. ): Promise<E.Either<unknown, void>> {
  368. if (!isValidRequestHandle(requestHandle, this.providerID, "personal")) {
  369. return Promise.resolve(E.left("INVALID_REQUEST_HANDLE" as const))
  370. }
  371. delete updatedRequest.id
  372. const { collectionID, requestID, request } = requestHandle.value.data
  373. const newRequest: HoppRESTRequest = merge(request, updatedRequest)
  374. const requestIndex = parseInt(requestID.split("/").slice(-1)[0])
  375. editRESTRequest(collectionID, requestIndex, newRequest)
  376. platform.analytics?.logEvent({
  377. type: "HOPP_SAVE_REQUEST",
  378. platform: "rest",
  379. createdNow: false,
  380. workspaceType: "personal",
  381. })
  382. for (const [idx, handle] of this.issuedHandles.entries()) {
  383. if (handle.value.type === "invalid") continue
  384. if ("requestID" in handle.value.data) {
  385. if (handle.value.data.requestID === requestID) {
  386. // @ts-expect-error - We're updating the request data
  387. this.issuedHandles[idx].value.data.request = newRequest
  388. }
  389. }
  390. }
  391. return Promise.resolve(E.right(undefined))
  392. }
  393. public importRESTCollections(
  394. workspaceHandle: HandleRef<Workspace>,
  395. collections: HoppCollection[]
  396. ): Promise<E.Either<unknown, HandleRef<WorkspaceCollection>>> {
  397. if (!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")) {
  398. return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
  399. }
  400. appendRESTCollections(collections)
  401. const newCollectionName = collections[0].name
  402. const newCollectionID =
  403. this.restCollectionState.value.state.length.toString()
  404. return Promise.resolve(
  405. E.right(
  406. computed(() => {
  407. if (
  408. !isValidWorkspaceHandle(
  409. workspaceHandle,
  410. this.providerID,
  411. "personal"
  412. )
  413. ) {
  414. return {
  415. type: "invalid" as const,
  416. reason: "WORKSPACE_INVALIDATED" as const,
  417. }
  418. }
  419. return {
  420. type: "ok",
  421. data: {
  422. providerID: this.providerID,
  423. workspaceID: workspaceHandle.value.data.workspaceID,
  424. collectionID: newCollectionID,
  425. name: newCollectionName,
  426. },
  427. }
  428. })
  429. )
  430. )
  431. }
  432. public exportRESTCollections(
  433. workspaceHandle: HandleRef<WorkspaceCollection>,
  434. collections: HoppCollection[]
  435. ): Promise<E.Either<unknown, void>> {
  436. if (!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")) {
  437. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  438. }
  439. initializeDownloadFile(JSON.stringify(collections, null, 2), "Collections")
  440. return Promise.resolve(E.right(undefined))
  441. }
  442. public exportRESTCollection(
  443. collectionHandle: HandleRef<WorkspaceCollection>,
  444. collection: HoppCollection
  445. ): Promise<E.Either<unknown, void>> {
  446. if (
  447. !isValidCollectionHandle(collectionHandle, this.providerID, "personal")
  448. ) {
  449. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  450. }
  451. initializeDownloadFile(JSON.stringify(collection, null, 2), collection.name)
  452. return Promise.resolve(E.right(undefined))
  453. }
  454. public reorderRESTCollection(
  455. collectionHandle: HandleRef<WorkspaceCollection>,
  456. destinationCollectionID: string | null
  457. ): Promise<E.Either<unknown, void>> {
  458. if (
  459. !isValidCollectionHandle(collectionHandle, this.providerID, "personal")
  460. ) {
  461. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  462. }
  463. const draggedCollectionIndex = collectionHandle.value.data.collectionID
  464. updateRESTCollectionOrder(draggedCollectionIndex, destinationCollectionID)
  465. return Promise.resolve(E.right(undefined))
  466. }
  467. public moveRESTCollection(
  468. collectionHandle: HandleRef<WorkspaceCollection>,
  469. destinationCollectionID: string | null
  470. ): Promise<E.Either<unknown, void>> {
  471. if (
  472. !isValidCollectionHandle(collectionHandle, this.providerID, "personal")
  473. ) {
  474. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  475. }
  476. moveRESTFolder(
  477. collectionHandle.value.data.collectionID,
  478. destinationCollectionID
  479. )
  480. return Promise.resolve(E.right(undefined))
  481. }
  482. public reorderRESTRequest(
  483. requestHandle: HandleRef<WorkspaceRequest>,
  484. destinationCollectionID: string,
  485. destinationRequestID: string | null
  486. ): Promise<E.Either<unknown, void>> {
  487. if (!isValidRequestHandle(requestHandle, this.providerID, "personal")) {
  488. return Promise.resolve(E.left("INVALID_REQUEST_HANDLE" as const))
  489. }
  490. const draggedRequestIndex = requestHandle.value.data.requestID
  491. updateRESTRequestOrder(
  492. this.pathToLastIndex(draggedRequestIndex),
  493. destinationRequestID ? this.pathToLastIndex(destinationRequestID) : null,
  494. destinationCollectionID
  495. )
  496. return Promise.resolve(E.right(undefined))
  497. }
  498. public moveRESTRequest(
  499. requestHandle: HandleRef<WorkspaceRequest>,
  500. destinationCollectionID: string
  501. ): Promise<E.Either<unknown, void>> {
  502. if (!isValidRequestHandle(requestHandle, this.providerID, "personal")) {
  503. return Promise.resolve(E.left("INVALID_REQUEST_HANDLE" as const))
  504. }
  505. const requestIndex = requestHandle.value.data.requestID
  506. const parentCollectionIndexPath = requestIndex
  507. .split("/")
  508. .slice(0, -1)
  509. .join("/")
  510. moveRESTRequest(
  511. parentCollectionIndexPath,
  512. this.pathToLastIndex(requestIndex),
  513. destinationCollectionID
  514. )
  515. return Promise.resolve(E.right(undefined))
  516. }
  517. public getCollectionHandle(
  518. workspaceHandle: HandleRef<Workspace>,
  519. collectionID: string
  520. ): Promise<E.Either<unknown, HandleRef<WorkspaceCollection>>> {
  521. if (!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")) {
  522. return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
  523. }
  524. if (collectionID === "") {
  525. return Promise.resolve(E.left("INVALID_COLLECTION_ID" as const))
  526. }
  527. const collection = navigateToFolderWithIndexPath(
  528. this.restCollectionState.value.state,
  529. collectionID.split("/").map((x) => parseInt(x))
  530. )
  531. if (!collection) {
  532. return Promise.resolve(E.left("COLLECTION_NOT_FOUND"))
  533. }
  534. const { providerID, workspaceID } = workspaceHandle.value.data
  535. return Promise.resolve(
  536. E.right(
  537. computed(() => {
  538. if (
  539. !isValidWorkspaceHandle(
  540. workspaceHandle,
  541. this.providerID,
  542. "personal"
  543. )
  544. ) {
  545. return {
  546. type: "invalid" as const,
  547. reason: "WORKSPACE_INVALIDATED" as const,
  548. }
  549. }
  550. return {
  551. type: "ok",
  552. data: {
  553. providerID,
  554. workspaceID,
  555. collectionID,
  556. name: collection.name,
  557. },
  558. }
  559. })
  560. )
  561. )
  562. }
  563. public getRequestHandle(
  564. workspaceHandle: HandleRef<Workspace>,
  565. requestID: string
  566. ): Promise<E.Either<unknown, HandleRef<WorkspaceRequest>>> {
  567. if (!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")) {
  568. return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
  569. }
  570. if (requestID === "") {
  571. return Promise.resolve(E.left("INVALID_REQUEST_ID" as const))
  572. }
  573. const { providerID, workspaceID } = workspaceHandle.value.data
  574. const collectionID = requestID.split("/").slice(0, -1).join("/")
  575. const requestIndexPath = requestID.split("/").slice(-1)[0]
  576. if (!requestIndexPath) {
  577. return Promise.resolve(E.left("INVALID_REQUEST_ID" as const))
  578. }
  579. const requestIndex = parseInt(requestIndexPath)
  580. // Navigate to the collection containing the request
  581. const collection = navigateToFolderWithIndexPath(
  582. this.restCollectionState.value.state,
  583. collectionID.split("/").map((x) => parseInt(x))
  584. )
  585. // Grab the request with it's index
  586. const request = collection?.requests[requestIndex] as
  587. | HoppRESTRequest
  588. | undefined
  589. if (!request) {
  590. return Promise.resolve(E.left("REQUEST_NOT_FOUND" as const))
  591. }
  592. const handleRefData = ref({
  593. type: "ok" as const,
  594. data: {
  595. providerID,
  596. workspaceID,
  597. collectionID,
  598. requestID,
  599. request,
  600. },
  601. })
  602. const handle: HandleRef<WorkspaceRequest> = computed(() => {
  603. if (
  604. !isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")
  605. ) {
  606. return {
  607. type: "invalid" as const,
  608. reason: "WORKSPACE_INVALIDATED" as const,
  609. }
  610. }
  611. return handleRefData.value
  612. })
  613. const writableHandle = computed({
  614. get() {
  615. return handleRefData.value
  616. },
  617. set(newValue) {
  618. handleRefData.value = newValue
  619. },
  620. })
  621. this.issuedHandles.push(writableHandle)
  622. return Promise.resolve(E.right(handle))
  623. }
  624. public getRESTCollectionChildrenView(
  625. collectionHandle: HandleRef<WorkspaceCollection>
  626. ): Promise<E.Either<never, HandleRef<RESTCollectionChildrenView>>> {
  627. return Promise.resolve(
  628. E.right(
  629. computed(() => {
  630. if (
  631. !isValidCollectionHandle(
  632. collectionHandle,
  633. this.providerID,
  634. "personal"
  635. )
  636. ) {
  637. return {
  638. type: "invalid" as const,
  639. reason: "INVALID_COLLECTION_HANDLE" as const,
  640. }
  641. }
  642. const collectionID = collectionHandle.value.data.collectionID
  643. return markRaw({
  644. type: "ok" as const,
  645. data: {
  646. providerID: this.providerID,
  647. workspaceID: collectionHandle.value.data.workspaceID,
  648. collectionID: collectionHandle.value.data.collectionID,
  649. loading: ref(false),
  650. content: computed(() => {
  651. const indexPath = collectionID
  652. .split("/")
  653. .map((x) => parseInt(x))
  654. const item = navigateToFolderWithIndexPath(
  655. this.restCollectionState.value.state,
  656. indexPath
  657. )
  658. if (item) {
  659. const collections = item.folders.map((childColl, id) => {
  660. return <RESTCollectionViewItem>{
  661. type: "collection",
  662. value: {
  663. collectionID: `${collectionID}/${id}`,
  664. isLastItem:
  665. item.folders?.length > 1
  666. ? id === item.folders.length - 1
  667. : false,
  668. name: childColl.name,
  669. parentCollectionID: collectionID,
  670. },
  671. }
  672. })
  673. const requests = item.requests.map((req, id) => {
  674. // TODO: Replace `parentCollectionID` with `collectionID`
  675. return <RESTCollectionViewItem>{
  676. type: "request",
  677. value: {
  678. isLastItem:
  679. item.requests?.length > 1
  680. ? id === item.requests.length - 1
  681. : false,
  682. collectionID,
  683. requestID: `${collectionID}/${id}`,
  684. request: req,
  685. },
  686. }
  687. })
  688. return [...collections, ...requests]
  689. }
  690. return []
  691. }),
  692. },
  693. })
  694. })
  695. )
  696. )
  697. }
  698. public getRESTRootCollectionView(
  699. workspaceHandle: HandleRef<Workspace>
  700. ): Promise<E.Either<never, HandleRef<RootRESTCollectionView>>> {
  701. return Promise.resolve(
  702. E.right(
  703. computed(() => {
  704. if (
  705. !isValidWorkspaceHandle(
  706. workspaceHandle,
  707. this.providerID,
  708. "personal"
  709. )
  710. ) {
  711. return {
  712. type: "invalid" as const,
  713. reason: "INVALID_WORKSPACE_HANDLE" as const,
  714. }
  715. }
  716. return markRaw({
  717. type: "ok" as const,
  718. data: {
  719. providerID: this.providerID,
  720. workspaceID: workspaceHandle.value.data.workspaceID,
  721. loading: ref(false),
  722. collections: computed(() => {
  723. return this.restCollectionState.value.state.map((coll, id) => {
  724. return {
  725. collectionID: id.toString(),
  726. isLastItem:
  727. id === this.restCollectionState.value.state.length - 1,
  728. name: coll.name,
  729. parentCollectionID: null,
  730. }
  731. })
  732. }),
  733. },
  734. })
  735. })
  736. )
  737. )
  738. }
  739. public getRESTCollectionLevelAuthHeadersView(
  740. collectionHandle: HandleRef<WorkspaceCollection>
  741. ): Promise<E.Either<never, HandleRef<RESTCollectionLevelAuthHeadersView>>> {
  742. return Promise.resolve(
  743. E.right(
  744. computed(() => {
  745. if (
  746. !isValidCollectionHandle(
  747. collectionHandle,
  748. this.providerID,
  749. "personal"
  750. )
  751. ) {
  752. return {
  753. type: "invalid" as const,
  754. reason: "INVALID_COLLECTION_HANDLE" as const,
  755. }
  756. }
  757. const { collectionID } = collectionHandle.value.data
  758. let auth: HoppInheritedProperty["auth"] = {
  759. parentID: collectionID ?? "",
  760. parentName: "",
  761. inheritedAuth: {
  762. authType: "none",
  763. authActive: true,
  764. },
  765. }
  766. const headers: HoppInheritedProperty["headers"] = []
  767. if (!collectionID) return { type: "ok", data: { auth, headers } }
  768. const path = collectionID.split("/").map((i) => parseInt(i))
  769. // Check if the path is empty or invalid
  770. if (!path || path.length === 0) {
  771. console.error("Invalid path:", collectionID)
  772. return { type: "ok", data: { auth, headers } }
  773. }
  774. // Loop through the path and get the last parent folder with authType other than 'inherit'
  775. for (let i = 0; i < path.length; i++) {
  776. const parentFolder = navigateToFolderWithIndexPath(
  777. this.restCollectionState.value.state,
  778. [...path.slice(0, i + 1)] // Create a copy of the path array
  779. )
  780. // Check if parentFolder is undefined or null
  781. if (!parentFolder) {
  782. console.error("Parent folder not found for path:", path)
  783. return { type: "ok", data: { auth, headers } }
  784. }
  785. const parentFolderAuth: HoppRESTAuth = parentFolder.auth
  786. const parentFolderHeaders: HoppRESTHeaders = parentFolder.headers
  787. // check if the parent folder has authType 'inherit' and if it is the root folder
  788. if (
  789. parentFolderAuth?.authType === "inherit" &&
  790. [...path.slice(0, i + 1)].length === 1
  791. ) {
  792. auth = {
  793. parentID: [...path.slice(0, i + 1)].join("/"),
  794. parentName: parentFolder.name,
  795. inheritedAuth: auth.inheritedAuth,
  796. }
  797. }
  798. if (parentFolderAuth?.authType !== "inherit") {
  799. auth = {
  800. parentID: [...path.slice(0, i + 1)].join("/"),
  801. parentName: parentFolder.name,
  802. inheritedAuth: parentFolderAuth,
  803. }
  804. }
  805. // Update headers, overwriting duplicates by key
  806. if (parentFolderHeaders) {
  807. const activeHeaders = parentFolderHeaders.filter((h) => h.active)
  808. activeHeaders.forEach((header) => {
  809. const index = headers.findIndex(
  810. (h) => h.inheritedHeader?.key === header.key
  811. )
  812. const currentPath = [...path.slice(0, i + 1)].join("/")
  813. if (index !== -1) {
  814. // Replace the existing header with the same key
  815. headers[index] = {
  816. parentID: currentPath,
  817. parentName: parentFolder.name,
  818. inheritedHeader: header,
  819. }
  820. } else {
  821. headers.push({
  822. parentID: currentPath,
  823. parentName: parentFolder.name,
  824. inheritedHeader: header,
  825. })
  826. }
  827. })
  828. }
  829. }
  830. return { type: "ok", data: { auth, headers } }
  831. })
  832. )
  833. )
  834. }
  835. public getRESTSearchResultsView(
  836. workspaceHandle: HandleRef<Workspace>,
  837. searchQuery: Ref<string>
  838. ): Promise<E.Either<never, HandleRef<RESTSearchResultsView>>> {
  839. const results = ref<HoppCollection[]>([])
  840. const isMatch = (inputText: string, textToMatch: string) =>
  841. inputText.toLowerCase().includes(textToMatch.toLowerCase())
  842. const filterRequests = (requests: HoppRESTRequest[]) => {
  843. return requests.filter((request) =>
  844. isMatch(request.name, searchQuery.value)
  845. )
  846. }
  847. const filterChildCollections = (
  848. childCollections: HoppCollection[]
  849. ): HoppCollection[] => {
  850. return childCollections
  851. .map((childCollection) => {
  852. // Render the entire collection tree if the search query matches a collection name
  853. if (isMatch(childCollection.name, searchQuery.value)) {
  854. return childCollection
  855. }
  856. const requests = filterRequests(
  857. childCollection.requests as HoppRESTRequest[]
  858. )
  859. const folders = filterChildCollections(childCollection.folders)
  860. return {
  861. ...childCollection,
  862. requests,
  863. folders,
  864. }
  865. })
  866. .filter(
  867. (childCollection) =>
  868. childCollection.requests.length > 0 ||
  869. childCollection.folders.length > 0 ||
  870. isMatch(childCollection.name, searchQuery.value)
  871. )
  872. }
  873. const scopeHandle = effectScope()
  874. scopeHandle.run(() => {
  875. watch(
  876. searchQuery,
  877. (newSearchQuery) => {
  878. if (!newSearchQuery) {
  879. results.value = this.restCollectionState.value.state
  880. return
  881. }
  882. const filteredCollections = this.restCollectionState.value.state
  883. .map((collection) => {
  884. // Render the entire collection tree if the search query matches a collection name
  885. if (isMatch(collection.name, searchQuery.value)) {
  886. return collection
  887. }
  888. const requests = filterRequests(
  889. collection.requests as HoppRESTRequest[]
  890. )
  891. const folders = filterChildCollections(collection.folders)
  892. return {
  893. ...collection,
  894. requests,
  895. folders,
  896. }
  897. })
  898. .filter(
  899. (collection) =>
  900. collection.requests.length > 0 ||
  901. collection.folders.length > 0 ||
  902. isMatch(collection.name, searchQuery.value)
  903. )
  904. results.value = filteredCollections
  905. },
  906. { immediate: true }
  907. )
  908. })
  909. const onSessionEnd = () => {
  910. scopeHandle.stop()
  911. }
  912. return Promise.resolve(
  913. E.right(
  914. computed(() => {
  915. if (
  916. !isValidWorkspaceHandle(
  917. workspaceHandle,
  918. this.providerID,
  919. "personal"
  920. )
  921. ) {
  922. return {
  923. type: "invalid" as const,
  924. reason: "INVALID_WORKSPACE_HANDLE" as const,
  925. }
  926. }
  927. return markRaw({
  928. type: "ok" as const,
  929. data: {
  930. providerID: this.providerID,
  931. workspaceID: workspaceHandle.value.data.workspaceID,
  932. loading: ref(false),
  933. results,
  934. onSessionEnd,
  935. },
  936. })
  937. })
  938. )
  939. )
  940. }
  941. public getRESTCollectionJSONView(
  942. workspaceHandle: HandleRef<Workspace>
  943. ): Promise<E.Either<never, HandleRef<RESTCollectionJSONView>>> {
  944. return Promise.resolve(
  945. E.right(
  946. computed(() => {
  947. if (
  948. !isValidWorkspaceHandle(
  949. workspaceHandle,
  950. this.providerID,
  951. "personal"
  952. )
  953. ) {
  954. return {
  955. type: "invalid" as const,
  956. reason: "INVALID_WORKSPACE_HANDLE" as const,
  957. }
  958. }
  959. return markRaw({
  960. type: "ok" as const,
  961. data: {
  962. providerID: this.providerID,
  963. workspaceID: workspaceHandle.value.data.workspaceID,
  964. content: JSON.stringify(
  965. this.restCollectionState.value.state,
  966. null,
  967. 2
  968. ),
  969. },
  970. })
  971. })
  972. )
  973. )
  974. }
  975. public getWorkspaceHandle(
  976. workspaceID: string
  977. ): Promise<E.Either<unknown, HandleRef<Workspace>>> {
  978. if (workspaceID !== "personal") {
  979. return Promise.resolve(E.left("INVALID_WORKSPACE_ID" as const))
  980. }
  981. return Promise.resolve(E.right(this.getPersonalWorkspaceHandle()))
  982. }
  983. public getPersonalWorkspaceHandle(): HandleRef<Workspace> {
  984. return shallowRef({
  985. type: "ok" as const,
  986. data: {
  987. providerID: this.providerID,
  988. workspaceID: "personal",
  989. name: "Personal Workspace",
  990. },
  991. })
  992. }
  993. }