import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform" import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections" import { runDispatchWithOutSyncing } from "../../lib/sync" import { exportUserCollectionsToJSON, runUserCollectionCreatedSubscription, runUserCollectionMovedSubscription, runUserCollectionOrderUpdatedSubscription, runUserCollectionRemovedSubscription, runUserCollectionUpdatedSubscription, runUserRequestCreatedSubscription, runUserRequestDeletedSubscription, runUserRequestMovedSubscription, runUserRequestUpdatedSubscription, } from "./collections.api" import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync" import * as E from "fp-ts/Either" import { addRESTCollection, setRESTCollections, editRESTCollection, removeRESTCollection, moveRESTFolder, updateRESTCollectionOrder, saveRESTRequestAs, navigateToFolderWithIndexPath, editRESTRequest, removeRESTRequest, moveRESTRequest, updateRESTRequestOrder, addRESTFolder, editRESTFolder, removeRESTFolder, addGraphqlFolder, addGraphqlCollection, editGraphqlFolder, editGraphqlCollection, removeGraphqlFolder, removeGraphqlCollection, saveGraphqlRequestAs, editGraphqlRequest, moveGraphqlRequest, removeGraphqlRequest, setGraphqlCollections, restCollectionStore, } from "@hoppscotch/common/newstore/collections" import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient" import { HoppCollection, HoppGQLRequest, HoppRESTRequest, } from "@hoppscotch/data" import { gqlCollectionsSyncer } from "./gqlCollections.sync" import { ReqType } from "../../api/generated/graphql" function initCollectionsSync() { const currentUser$ = platformAuth.getCurrentUserStream() collectionsSyncer.startStoreSync() collectionsSyncer.setupSubscriptions(setupSubscriptions) gqlCollectionsSyncer.startStoreSync() loadUserCollections("REST") loadUserCollections("GQL") // TODO: test & make sure the auth thing is working properly currentUser$.subscribe(async (user) => { if (user) { loadUserCollections("REST") loadUserCollections("GQL") } }) authEvents$.subscribe((event) => { if (event.event == "login" || event.event == "token_refresh") { collectionsSyncer.startListeningToSubscriptions() } if (event.event == "logout") { collectionsSyncer.stopListeningToSubscriptions() } }) } type ExportedUserCollectionREST = { id?: string folders: ExportedUserCollectionREST[] requests: Array name: string data: string } type ExportedUserCollectionGQL = { id?: string folders: ExportedUserCollectionGQL[] requests: Array name: string data: string } function exportedCollectionToHoppCollection( collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collectionType: "REST" | "GQL" ): HoppCollection { if (collectionType == "REST") { const restCollection = collection as ExportedUserCollectionREST const data = restCollection.data && restCollection.data !== "null" ? JSON.parse(restCollection.data) : { auth: { authType: "inherit", authActive: false }, headers: [], } return { id: restCollection.id, v: 2, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) ), requests: restCollection.requests.map( ({ id, v, auth, body, endpoint, headers, method, name, params, preRequestScript, testScript, }) => ({ id, v, auth, body, endpoint, headers, method, name, params, preRequestScript, testScript, }) ), auth: data.auth, headers: data.headers, } } else { const gqlCollection = collection as ExportedUserCollectionGQL const data = gqlCollection.data && gqlCollection.data !== "null" ? JSON.parse(gqlCollection.data) : { auth: { authType: "inherit", authActive: false }, headers: [], } return { id: gqlCollection.id, v: 2, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) ), requests: gqlCollection.requests.map( ({ v, auth, headers, name, id, query, url, variables }) => ({ id, v, auth, headers, name, query, url, variables, }) ) as HoppGQLRequest[], auth: data.auth, headers: data.headers, } } } async function loadUserCollections(collectionType: "REST" | "GQL") { const res = await exportUserCollectionsToJSON( undefined, collectionType == "REST" ? ReqType.Rest : ReqType.Gql ) if (E.isRight(res)) { const collectionsJSONString = res.right.exportUserCollectionsToJSON.exportedCollection const exportedCollections = ( JSON.parse(collectionsJSONString) as Array< ExportedUserCollectionGQL | ExportedUserCollectionREST > ).map((collection) => ({ v: 1, ...collection })) runDispatchWithOutSyncing(() => { collectionType == "REST" ? setRESTCollections( exportedCollections.map( (collection) => exportedCollectionToHoppCollection( collection, "REST" ) as HoppCollection ) ) : setGraphqlCollections( exportedCollections.map( (collection) => exportedCollectionToHoppCollection( collection, "GQL" ) as HoppCollection ) ) }) } } function setupSubscriptions() { let subs: ReturnType[1][] = [] const userCollectionCreatedSub = setupUserCollectionCreatedSubscription() const userCollectionUpdatedSub = setupUserCollectionUpdatedSubscription() const userCollectionRemovedSub = setupUserCollectionRemovedSubscription() const userCollectionMovedSub = setupUserCollectionMovedSubscription() const userCollectionOrderUpdatedSub = setupUserCollectionOrderUpdatedSubscription() const userRequestCreatedSub = setupUserRequestCreatedSubscription() const userRequestUpdatedSub = setupUserRequestUpdatedSubscription() const userRequestDeletedSub = setupUserRequestDeletedSubscription() const userRequestMovedSub = setupUserRequestMovedSubscription() subs = [ userCollectionCreatedSub, userCollectionUpdatedSub, userCollectionRemovedSub, userCollectionMovedSub, userCollectionOrderUpdatedSub, userRequestCreatedSub, userRequestUpdatedSub, userRequestDeletedSub, userRequestMovedSub, ] return () => { subs.forEach((sub) => sub.unsubscribe()) } } function setupUserCollectionCreatedSubscription() { const [userCollectionCreated$, userCollectionCreatedSub] = runUserCollectionCreatedSubscription() userCollectionCreated$.subscribe((res) => { if (E.isRight(res)) { const collectionType = res.right.userCollectionCreated.type const { collectionStore } = getStoreByCollectionType(collectionType) const userCollectionBackendID = res.right.userCollectionCreated.id const parentCollectionID = res.right.userCollectionCreated.parent?.id const userCollectionLocalID = getCollectionPathFromCollectionID( userCollectionBackendID, collectionStore.value.state ) // collection already exists in store ( this instance created it ) if (userCollectionLocalID) { return } const parentCollectionPath = parentCollectionID && getCollectionPathFromCollectionID( parentCollectionID, collectionStore.value.state ) // only folders will have parent collection id if (parentCollectionID && parentCollectionPath) { runDispatchWithOutSyncing(() => { collectionType == "GQL" ? addGraphqlFolder( res.right.userCollectionCreated.title, parentCollectionPath ) : addRESTFolder( res.right.userCollectionCreated.title, parentCollectionPath ) const parentCollection = navigateToFolderWithIndexPath( collectionStore.value.state, parentCollectionPath .split("/") .map((pathIndex) => parseInt(pathIndex)) ) if (parentCollection) { const folderIndex = parentCollection.folders.length - 1 const addedFolder = parentCollection.folders[folderIndex] addedFolder.id = userCollectionBackendID } }) } else { // root collections won't have parentCollectionID const data = res.right.userCollectionCreated.data && res.right.userCollectionCreated.data != "null" ? JSON.parse(res.right.userCollectionCreated.data) : { auth: { authType: "inherit", authActive: false }, headers: [], } runDispatchWithOutSyncing(() => { collectionType == "GQL" ? addGraphqlCollection({ name: res.right.userCollectionCreated.title, folders: [], requests: [], v: 2, auth: data.auth, headers: data.headers, }) : addRESTCollection({ name: res.right.userCollectionCreated.title, folders: [], requests: [], v: 2, auth: data.auth, headers: data?.headers, }) const localIndex = collectionStore.value.state.length - 1 const addedCollection = collectionStore.value.state[localIndex] addedCollection.id = userCollectionBackendID }) } } }) return userCollectionCreatedSub } function setupUserCollectionUpdatedSubscription() { const [userCollectionUpdated$, userCollectionUpdatedSub] = runUserCollectionUpdatedSubscription() userCollectionUpdated$.subscribe((res) => { if (E.isRight(res)) { const collectionType = res.right.userCollectionUpdated.type const { collectionStore } = getStoreByCollectionType(collectionType) const updatedCollectionBackendID = res.right.userCollectionUpdated.id const updatedCollectionLocalPath = getCollectionPathFromCollectionID( updatedCollectionBackendID, collectionStore.value.state ) const isFolder = updatedCollectionLocalPath && updatedCollectionLocalPath.split("/").length > 1 // updated collection is a folder if (isFolder) { runDispatchWithOutSyncing(() => { collectionType == "REST" ? editRESTFolder(updatedCollectionLocalPath, { name: res.right.userCollectionUpdated.title, }) : editGraphqlFolder(updatedCollectionLocalPath, { name: res.right.userCollectionUpdated.title, }) }) } // updated collection is a root collection if (updatedCollectionLocalPath && !isFolder) { runDispatchWithOutSyncing(() => { collectionType == "REST" ? editRESTCollection(parseInt(updatedCollectionLocalPath), { name: res.right.userCollectionUpdated.title, }) : editGraphqlCollection(parseInt(updatedCollectionLocalPath), { name: res.right.userCollectionUpdated.title, }) }) } } }) return userCollectionUpdatedSub } function setupUserCollectionMovedSubscription() { const [userCollectionMoved$, userCollectionMovedSub] = runUserCollectionMovedSubscription() userCollectionMoved$.subscribe((res) => { if (E.isRight(res)) { const movedMetadata = res.right.userCollectionMoved const sourcePath = getCollectionPathFromCollectionID( movedMetadata.id, restCollectionStore.value.state ) let destinationPath: string | undefined if (movedMetadata.parent?.id) { destinationPath = getCollectionPathFromCollectionID( movedMetadata.parent?.id, restCollectionStore.value.state ) ?? undefined } sourcePath && runDispatchWithOutSyncing(() => { moveRESTFolder(sourcePath, destinationPath ?? null) }) } }) return userCollectionMovedSub } function setupUserCollectionRemovedSubscription() { const [userCollectionRemoved$, userCollectionRemovedSub] = runUserCollectionRemovedSubscription() userCollectionRemoved$.subscribe((res) => { if (E.isRight(res)) { const removedCollectionBackendID = res.right.userCollectionRemoved.id const collectionType = res.right.userCollectionRemoved.type const { collectionStore } = getStoreByCollectionType(collectionType) const removedCollectionLocalPath = getCollectionPathFromCollectionID( removedCollectionBackendID, collectionStore.value.state ) const isFolder = removedCollectionLocalPath && removedCollectionLocalPath.split("/").length > 1 if (removedCollectionLocalPath && isFolder) { runDispatchWithOutSyncing(() => { collectionType == "REST" ? removeRESTFolder(removedCollectionLocalPath) : removeGraphqlFolder(removedCollectionLocalPath) }) } if (removedCollectionLocalPath && !isFolder) { runDispatchWithOutSyncing(() => { collectionType == "REST" ? removeRESTCollection(parseInt(removedCollectionLocalPath)) : removeGraphqlCollection(parseInt(removedCollectionLocalPath)) }) } } }) return userCollectionRemovedSub } function setupUserCollectionOrderUpdatedSubscription() { const [userCollectionOrderUpdated$, userCollectionOrderUpdatedSub] = runUserCollectionOrderUpdatedSubscription() userCollectionOrderUpdated$.subscribe((res) => { if (E.isRight(res)) { const { userCollection, nextUserCollection } = res.right.userCollectionOrderUpdated const sourceCollectionID = userCollection.id const destinationCollectionID = nextUserCollection?.id const sourcePath = getCollectionPathFromCollectionID( sourceCollectionID, restCollectionStore.value.state ) let destinationPath: string | null | undefined if (destinationCollectionID) { destinationPath = getCollectionPathFromCollectionID( destinationCollectionID, restCollectionStore.value.state ) } runDispatchWithOutSyncing(() => { if (sourcePath) { updateRESTCollectionOrder(sourcePath, destinationPath ?? null) } }) } }) return userCollectionOrderUpdatedSub } function setupUserRequestCreatedSubscription() { const [userRequestCreated$, userRequestCreatedSub] = runUserRequestCreatedSubscription() userRequestCreated$.subscribe((res) => { if (E.isRight(res)) { const collectionID = res.right.userRequestCreated.collectionID const request = JSON.parse(res.right.userRequestCreated.request) const requestID = res.right.userRequestCreated.id const requestType = res.right.userRequestCreated.type const { collectionStore } = getStoreByCollectionType(requestType) const hasAlreadyHappened = getRequestPathFromRequestID( requestID, collectionStore.value.state ) if (!!hasAlreadyHappened) { return } const collectionPath = getCollectionPathFromCollectionID( collectionID, collectionStore.value.state ) if (collectionID && collectionPath) { runDispatchWithOutSyncing(() => { requestType == "REST" ? saveRESTRequestAs(collectionPath, request) : saveGraphqlRequestAs(collectionPath, request) const target = navigateToFolderWithIndexPath( collectionStore.value.state, collectionPath.split("/").map((index) => parseInt(index)) ) const targetRequest = target?.requests[target?.requests.length - 1] if (targetRequest) { targetRequest.id = requestID } }) } } }) return userRequestCreatedSub } function setupUserRequestUpdatedSubscription() { const [userRequestUpdated$, userRequestUpdatedSub] = runUserRequestUpdatedSubscription() userRequestUpdated$.subscribe((res) => { if (E.isRight(res)) { const requestType = res.right.userRequestUpdated.type const { collectionStore } = getStoreByCollectionType(requestType) const requestPath = getRequestPathFromRequestID( res.right.userRequestUpdated.id, collectionStore.value.state ) const collectionPath = requestPath?.collectionPath const requestIndex = requestPath?.requestIndex ;(requestIndex || requestIndex == 0) && collectionPath && runDispatchWithOutSyncing(() => { requestType == "REST" ? editRESTRequest( collectionPath, requestIndex, JSON.parse(res.right.userRequestUpdated.request) ) : editGraphqlRequest( collectionPath, requestIndex, JSON.parse(res.right.userRequestUpdated.request) ) }) } }) return userRequestUpdatedSub } function setupUserRequestMovedSubscription() { const [userRequestMoved$, userRequestMovedSub] = runUserRequestMovedSubscription() userRequestMoved$.subscribe((res) => { if (E.isRight(res)) { const { request, nextRequest } = res.right.userRequestMoved const { collectionID: destinationCollectionID, id: sourceRequestID, type: requestType, } = request const { collectionStore } = getStoreByCollectionType(requestType) const sourceRequestPath = getRequestPathFromRequestID( sourceRequestID, collectionStore.value.state ) const destinationCollectionPath = getCollectionPathFromCollectionID( destinationCollectionID, collectionStore.value.state ) const destinationRequestIndex = destinationCollectionPath ? (() => { const requestsLength = navigateToFolderWithIndexPath( collectionStore.value.state, destinationCollectionPath .split("/") .map((index) => parseInt(index)) )?.requests.length return requestsLength || requestsLength == 0 ? requestsLength - 1 : undefined })() : undefined // there is no nextRequest, so request is moved if ( (destinationRequestIndex || destinationRequestIndex == 0) && destinationCollectionPath && sourceRequestPath && !nextRequest ) { runDispatchWithOutSyncing(() => { requestType == "REST" ? moveRESTRequest( sourceRequestPath.collectionPath, sourceRequestPath.requestIndex, destinationCollectionPath ) : moveGraphqlRequest( sourceRequestPath.collectionPath, sourceRequestPath.requestIndex, destinationCollectionPath ) }) } // there is nextRequest, so request is reordered if ( (destinationRequestIndex || destinationRequestIndex == 0) && destinationCollectionPath && nextRequest && // we don't have request reordering for graphql yet requestType == "REST" ) { const { collectionID: nextCollectionID, id: nextRequestID } = nextRequest const nextCollectionPath = getCollectionPathFromCollectionID( nextCollectionID, collectionStore.value.state ) ?? undefined const nextRequestIndex = nextCollectionPath ? getRequestIndex( nextRequestID, nextCollectionPath, collectionStore.value.state ) : undefined nextRequestIndex && nextCollectionPath && sourceRequestPath && runDispatchWithOutSyncing(() => { updateRESTRequestOrder( sourceRequestPath?.requestIndex, nextRequestIndex, nextCollectionPath ) }) } } }) return userRequestMovedSub } function setupUserRequestDeletedSubscription() { const [userRequestDeleted$, userRequestDeletedSub] = runUserRequestDeletedSubscription() userRequestDeleted$.subscribe((res) => { if (E.isRight(res)) { const requestType = res.right.userRequestDeleted.type const { collectionStore } = getStoreByCollectionType(requestType) const deletedRequestPath = getRequestPathFromRequestID( res.right.userRequestDeleted.id, collectionStore.value.state ) ;(deletedRequestPath?.requestIndex || deletedRequestPath?.requestIndex == 0) && deletedRequestPath.collectionPath && runDispatchWithOutSyncing(() => { requestType == "REST" ? removeRESTRequest( deletedRequestPath.collectionPath, deletedRequestPath.requestIndex ) : removeGraphqlRequest( deletedRequestPath.collectionPath, deletedRequestPath.requestIndex ) }) } }) return userRequestDeletedSub } export const def: CollectionsPlatformDef = { initCollectionsSync, } function getCollectionPathFromCollectionID( collectionID: string, collections: HoppCollection[], parentPath?: string ): string | null { for (const collectionIndex in collections) { if (collections[collectionIndex].id == collectionID) { return parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}` } else { const collectionPath = getCollectionPathFromCollectionID( collectionID, collections[collectionIndex].folders, parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}` ) if (collectionPath) return collectionPath } } return null } function getRequestPathFromRequestID( requestID: string, collections: HoppCollection[], parentPath?: string ): { collectionPath: string; requestIndex: number } | null { for (const collectionIndex in collections) { const requestIndex = collections[collectionIndex].requests.findIndex( (request) => request.id == requestID ) if (requestIndex != -1) { return { collectionPath: parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}`, requestIndex, } } else { const requestPath = getRequestPathFromRequestID( requestID, collections[collectionIndex].folders, parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}` ) if (requestPath) return requestPath } } return null } function getRequestIndex( requestID: string, parentCollectionPath: string, collections: HoppCollection[] ) { const collection = navigateToFolderWithIndexPath( collections, parentCollectionPath?.split("/").map((index) => parseInt(index)) ) const requestIndex = collection?.requests.findIndex( (request) => request.id == requestID ) return requestIndex }