123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- import * as E from "fp-ts/Either"
- import { BehaviorSubject, Subscription } from "rxjs"
- import { translateToNewRequest } from "@hoppscotch/data"
- import pull from "lodash/pull"
- import remove from "lodash/remove"
- import { Subscription as WSubscription } from "wonka"
- import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
- import { TeamCollection } from "./TeamCollection"
- import { TeamRequest } from "./TeamRequest"
- import {
- RootCollectionsOfTeamDocument,
- TeamCollectionAddedDocument,
- TeamCollectionUpdatedDocument,
- TeamCollectionRemovedDocument,
- TeamRequestAddedDocument,
- TeamRequestUpdatedDocument,
- TeamRequestDeletedDocument,
- GetCollectionChildrenDocument,
- GetCollectionRequestsDocument,
- } from "~/helpers/backend/graphql"
- const TEAMS_BACKEND_PAGE_SIZE = 10
- /**
- * Finds the parent of a collection and returns the REFERENCE (or null)
- *
- * @param {TeamCollection[]} tree - The tree to look in
- * @param {string} collID - ID of the collection to find the parent of
- * @param {TeamCollection} currentParent - (used for recursion, do not set) The parent in the current iteration (undefined if root)
- *
- * @returns REFERENCE to the collection or null if not found or the collection is in root
- */
- function findParentOfColl(
- tree: TeamCollection[],
- collID: string,
- currentParent?: TeamCollection
- ): TeamCollection | null {
- for (const coll of tree) {
- // If the root is parent, return null
- if (coll.id === collID) return currentParent || null
- // Else run it in children
- if (coll.children) {
- const result = findParentOfColl(coll.children, collID, coll)
- if (result) return result
- }
- }
- return null
- }
- /**
- * Finds and returns a REFERENCE collection in the given tree (or null)
- *
- * @param {TeamCollection[]} tree - The tree to look in
- * @param {string} targetID - The ID of the collection to look for
- *
- * @returns REFERENCE to the collection or null if not found
- */
- function findCollInTree(
- tree: TeamCollection[],
- targetID: string
- ): TeamCollection | null {
- for (const coll of tree) {
- // If the direct child matched, then return that
- if (coll.id === targetID) return coll
- // Else run it in the children
- if (coll.children) {
- const result = findCollInTree(coll.children, targetID)
- if (result) return result
- }
- }
- // If nothing matched, return null
- return null
- }
- /**
- * Deletes a collection in the tree
- *
- * @param {TeamCollection[]} tree - The tree to delete in (THIS WILL BE MUTATED!)
- * @param {string} targetID - ID of the collection to delete
- */
- function deleteCollInTree(tree: TeamCollection[], targetID: string) {
- // Get the parent owning the collection
- const parent = findParentOfColl(tree, targetID)
- // If we found a parent, update it
- if (parent && parent.children) {
- parent.children = parent.children.filter((coll) => coll.id !== targetID)
- }
- // If there is no parent, it could mean:
- // 1. The collection with that ID does not exist
- // 2. The collection is in root (therefore, no parent)
- // Let's look for element, if not exist, then stop
- const el = findCollInTree(tree, targetID)
- if (!el) return
- // Collection exists, so this should be in root, hence removing element
- pull(tree, el)
- }
- /**
- * Updates a collection in the tree with the specified data
- *
- * @param {TeamCollection[]} tree - The tree to update in (THIS WILL BE MUTATED!)
- * @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} updateColl - An object defining all the fields that should be updated (ID is required to find the target collection)
- */
- function updateCollInTree(
- tree: TeamCollection[],
- updateColl: Partial<TeamCollection> & Pick<TeamCollection, "id">
- ) {
- const el = findCollInTree(tree, updateColl.id)
- // If no match, stop the operation
- if (!el) return
- // Update all the specified keys
- Object.assign(el, updateColl)
- }
- /**
- * Finds and returns a REFERENCE to the request with the given ID (or null)
- *
- * @param {TeamCollection[]} tree - The tree to look in
- * @param {string} reqID - The ID of the request to look for
- *
- * @returns REFERENCE to the request or null if request not found
- */
- function findReqInTree(
- tree: TeamCollection[],
- reqID: string
- ): TeamRequest | null {
- for (const coll of tree) {
- // Check in root collections (if expanded)
- if (coll.requests) {
- const match = coll.requests.find((req) => req.id === reqID)
- if (match) return match
- }
- // Check in children of collections
- if (coll.children) {
- const match = findReqInTree(coll.children, reqID)
- if (match) return match
- }
- }
- // No matches
- return null
- }
- /**
- * Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null)
- *
- * @param {TeamCollection[]} tree - The tree to look in
- * @param {string} reqID - The ID of the request to look for
- *
- * @returns REFERENCE to the collection or null if request not found
- */
- function findCollWithReqIDInTree(
- tree: TeamCollection[],
- reqID: string
- ): TeamCollection | null {
- for (const coll of tree) {
- // Check in root collections (if expanded)
- if (coll.requests) {
- if (coll.requests.find((req) => req.id === reqID)) return coll
- }
- // Check in children of collections
- if (coll.children) {
- const result = findCollWithReqIDInTree(coll.children, reqID)
- if (result) return result
- }
- }
- // No matches
- return null
- }
- export default class NewTeamCollectionAdapter {
- collections$: BehaviorSubject<TeamCollection[]>
- // Stream to the list of collections/folders that are being loaded in
- loadingCollections$: BehaviorSubject<string[]>
- private teamCollectionAdded$: Subscription | null
- private teamCollectionUpdated$: Subscription | null
- private teamCollectionRemoved$: Subscription | null
- private teamRequestAdded$: Subscription | null
- private teamRequestUpdated$: Subscription | null
- private teamRequestDeleted$: Subscription | null
- private teamCollectionAddedSub: WSubscription | null
- private teamCollectionUpdatedSub: WSubscription | null
- private teamCollectionRemovedSub: WSubscription | null
- private teamRequestAddedSub: WSubscription | null
- private teamRequestUpdatedSub: WSubscription | null
- private teamRequestDeletedSub: WSubscription | null
- constructor(private teamID: string | null) {
- this.collections$ = new BehaviorSubject<TeamCollection[]>([])
- this.loadingCollections$ = new BehaviorSubject<string[]>([])
- this.teamCollectionAdded$ = null
- this.teamCollectionUpdated$ = null
- this.teamCollectionRemoved$ = null
- this.teamRequestAdded$ = null
- this.teamRequestDeleted$ = null
- this.teamRequestUpdated$ = null
- this.teamCollectionAddedSub = null
- this.teamCollectionUpdatedSub = null
- this.teamCollectionRemovedSub = null
- this.teamRequestAddedSub = null
- this.teamRequestDeletedSub = null
- this.teamRequestUpdatedSub = null
- if (this.teamID) this.initialize()
- }
- changeTeamID(newTeamID: string | null) {
- this.teamID = newTeamID
- this.collections$.next([])
- this.loadingCollections$.next([])
- this.unsubscribeSubscriptions()
- if (this.teamID) this.initialize()
- }
- /**
- * Unsubscribes from the subscriptions
- * NOTE: Once this is called, no new updates to the tree will be detected
- */
- unsubscribeSubscriptions() {
- this.teamCollectionAdded$?.unsubscribe()
- this.teamCollectionUpdated$?.unsubscribe()
- this.teamCollectionRemoved$?.unsubscribe()
- this.teamRequestAdded$?.unsubscribe()
- this.teamRequestDeleted$?.unsubscribe()
- this.teamRequestUpdated$?.unsubscribe()
- this.teamCollectionAddedSub?.unsubscribe()
- this.teamCollectionUpdatedSub?.unsubscribe()
- this.teamCollectionRemovedSub?.unsubscribe()
- this.teamRequestAddedSub?.unsubscribe()
- this.teamRequestDeletedSub?.unsubscribe()
- this.teamRequestUpdatedSub?.unsubscribe()
- }
- private async initialize() {
- await this.loadRootCollections()
- this.registerSubscriptions()
- }
- /**
- * Performs addition of a collection to the tree
- *
- * @param {TeamCollection} collection - The collection to add to the tree
- * @param {string | null} parentCollectionID - The parent of the new collection, pass null if this collection is in root
- */
- private addCollection(
- collection: TeamCollection,
- parentCollectionID: string | null
- ) {
- const tree = this.collections$.value
- if (!parentCollectionID) {
- tree.push(collection)
- } else {
- const parentCollection = findCollInTree(tree, parentCollectionID)
- if (!parentCollection) return
- if (parentCollection.children != null) {
- parentCollection.children.push(collection)
- } else {
- parentCollection.children = [collection]
- }
- }
- this.collections$.next(tree)
- }
- private async loadRootCollections() {
- if (this.teamID === null) throw new Error("Team ID is null")
- this.loadingCollections$.next([
- ...this.loadingCollections$.getValue(),
- "root",
- ])
- const totalCollections: TeamCollection[] = []
- while (true) {
- const result = await runGQLQuery({
- query: RootCollectionsOfTeamDocument,
- variables: {
- teamID: this.teamID,
- cursor:
- totalCollections.length > 0
- ? totalCollections[totalCollections.length - 1].id
- : undefined,
- },
- })
- if (E.isLeft(result)) {
- this.loadingCollections$.next(
- this.loadingCollections$.getValue().filter((x) => x !== "root")
- )
- throw new Error(`Error fetching root collections: ${result}`)
- }
- totalCollections.push(
- ...result.right.rootCollectionsOfTeam.map(
- (x) =>
- <TeamCollection>{
- ...x,
- children: null,
- requests: null,
- }
- )
- )
- if (result.right.rootCollectionsOfTeam.length !== TEAMS_BACKEND_PAGE_SIZE)
- break
- }
- this.loadingCollections$.next(
- this.loadingCollections$.getValue().filter((x) => x !== "root")
- )
- this.collections$.next(totalCollections)
- }
- /**
- * Updates an existing collection in tree
- *
- * @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} collectionUpdate - Object defining the fields that need to be updated (ID is required to find the target)
- */
- private updateCollection(
- collectionUpdate: Partial<TeamCollection> & Pick<TeamCollection, "id">
- ) {
- const tree = this.collections$.value
- updateCollInTree(tree, collectionUpdate)
- this.collections$.next(tree)
- }
- /**
- * Removes a collection from the tree
- *
- * @param {string} collectionID - ID of the collection to remove
- */
- private removeCollection(collectionID: string) {
- const tree = this.collections$.value
- deleteCollInTree(tree, collectionID)
- this.collections$.next(tree)
- }
- /**
- * Adds a request to the tree
- *
- * @param {TeamRequest} request - The request to add to the tree
- */
- private addRequest(request: TeamRequest) {
- const tree = this.collections$.value
- // Check if we have the collection (if not, then not loaded?)
- const coll = findCollInTree(tree, request.collectionID)
- if (!coll) return // Ignore add request
- // Collection is not expanded
- if (!coll.requests) return
- // Collection is expanded hence append request
- coll.requests.push(request)
- this.collections$.next(tree)
- }
- /**
- * Updates the request in tree
- *
- * @param {Partial<TeamRequest> & Pick<TeamRequest, 'id'>} requestUpdate - Object defining all the fields to update in request (ID of the request is required)
- */
- private updateRequest(
- requestUpdate: Partial<TeamRequest> & Pick<TeamRequest, "id">
- ) {
- const tree = this.collections$.value
- // Find request, if not present, don't update
- const req = findReqInTree(tree, requestUpdate.id)
- if (!req) return
- Object.assign(req, requestUpdate)
- this.collections$.next(tree)
- }
- /**
- * Removes a request from the tree
- *
- * @param {string} requestID - ID of the request to remove
- */
- private removeRequest(requestID: string) {
- const tree = this.collections$.value
- // Find request in tree, don't attempt if no collection or no requests (expansion?)
- const coll = findCollWithReqIDInTree(tree, requestID)
- if (!coll || !coll.requests) return
- // Remove the collection
- remove(coll.requests, (req) => req.id === requestID)
- // Publish new tree
- this.collections$.next(tree)
- }
- private registerSubscriptions() {
- if (!this.teamID) return
- const [teamCollAdded$, teamCollAddedSub] = runGQLSubscription({
- query: TeamCollectionAddedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamCollectionAddedSub = teamCollAddedSub
- this.teamCollectionAdded$ = teamCollAdded$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Collection Added Error: ${result.left}`)
- this.addCollection(
- {
- id: result.right.teamCollectionAdded.id,
- children: null,
- requests: null,
- title: result.right.teamCollectionAdded.title,
- },
- result.right.teamCollectionAdded.parent?.id ?? null
- )
- })
- const [teamCollUpdated$, teamCollUpdatedSub] = runGQLSubscription({
- query: TeamCollectionUpdatedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamCollectionUpdatedSub = teamCollUpdatedSub
- this.teamCollectionUpdated$ = teamCollUpdated$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Collection Updated Error: ${result.left}`)
- this.updateCollection({
- id: result.right.teamCollectionUpdated.id,
- title: result.right.teamCollectionUpdated.title,
- })
- })
- const [teamCollRemoved$, teamCollRemovedSub] = runGQLSubscription({
- query: TeamCollectionRemovedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamCollectionRemovedSub = teamCollRemovedSub
- this.teamCollectionRemoved$ = teamCollRemoved$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Collection Removed Error: ${result.left}`)
- this.removeCollection(result.right.teamCollectionRemoved)
- })
- const [teamReqAdded$, teamReqAddedSub] = runGQLSubscription({
- query: TeamRequestAddedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamRequestAddedSub = teamReqAddedSub
- this.teamRequestAdded$ = teamReqAdded$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Request Added Error: ${result.left}`)
- this.addRequest({
- id: result.right.teamRequestAdded.id,
- collectionID: result.right.teamRequestAdded.collectionID,
- request: translateToNewRequest(
- JSON.parse(result.right.teamRequestAdded.request)
- ),
- title: result.right.teamRequestAdded.title,
- })
- })
- const [teamReqUpdated$, teamReqUpdatedSub] = runGQLSubscription({
- query: TeamRequestUpdatedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamRequestUpdatedSub = teamReqUpdatedSub
- this.teamRequestUpdated$ = teamReqUpdated$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Request Updated Error: ${result.left}`)
- this.updateRequest({
- id: result.right.teamRequestUpdated.id,
- collectionID: result.right.teamRequestUpdated.collectionID,
- request: JSON.parse(result.right.teamRequestUpdated.request),
- title: result.right.teamRequestUpdated.title,
- })
- })
- const [teamReqDeleted$, teamReqDeleted] = runGQLSubscription({
- query: TeamRequestDeletedDocument,
- variables: {
- teamID: this.teamID,
- },
- })
- this.teamRequestUpdatedSub = teamReqDeleted
- this.teamRequestDeleted$ = teamReqDeleted$.subscribe((result) => {
- if (E.isLeft(result))
- throw new Error(`Team Request Deleted Error ${result.left}`)
- this.removeRequest(result.right.teamRequestDeleted)
- })
- }
- /**
- * Expands a collection on the tree
- *
- * When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null)
- * Upon expansion those two fields will be populated
- *
- * @param {string} collectionID - The ID of the collection to expand
- */
- async expandCollection(collectionID: string): Promise<void> {
- // TODO: While expanding one collection, block (or queue) the expansion of the other, to avoid race conditions
- const tree = this.collections$.value
- const collection = findCollInTree(tree, collectionID)
- if (!collection) return
- if (collection.children != null) return
- const collections: TeamCollection[] = []
- this.loadingCollections$.next([
- ...this.loadingCollections$.getValue(),
- collectionID,
- ])
- while (true) {
- const data = await runGQLQuery({
- query: GetCollectionChildrenDocument,
- variables: {
- collectionID,
- cursor:
- collections.length > 0
- ? collections[collections.length - 1].id
- : undefined,
- },
- })
- if (E.isLeft(data)) {
- this.loadingCollections$.next(
- this.loadingCollections$.getValue().filter((x) => x !== collectionID)
- )
- throw new Error(
- `Child Collection Fetch Error for ${collectionID}: ${data.left}`
- )
- }
- collections.push(
- ...data.right.collection!.children.map(
- (el) =>
- <TeamCollection>{
- id: el.id,
- title: el.title,
- children: null,
- requests: null,
- }
- )
- )
- if (data.right.collection!.children.length !== TEAMS_BACKEND_PAGE_SIZE)
- break
- }
- const requests: TeamRequest[] = []
- while (true) {
- const data = await runGQLQuery({
- query: GetCollectionRequestsDocument,
- variables: {
- collectionID,
- cursor:
- requests.length > 0 ? requests[requests.length - 1].id : undefined,
- },
- })
- if (E.isLeft(data)) {
- this.loadingCollections$.next(
- this.loadingCollections$.getValue().filter((x) => x !== collectionID)
- )
- throw new Error(`Child Request Fetch Error for ${data}: ${data.left}`)
- }
- requests.push(
- ...data.right.requestsInCollection.map<TeamRequest>((el) => {
- return {
- id: el.id,
- collectionID,
- title: el.title,
- request: translateToNewRequest(JSON.parse(el.request)),
- }
- })
- )
- if (data.right.requestsInCollection.length !== TEAMS_BACKEND_PAGE_SIZE)
- break
- }
- collection.children = collections
- collection.requests = requests
- this.loadingCollections$.next(
- this.loadingCollections$.getValue().filter((x) => x !== collectionID)
- )
- this.collections$.next(tree)
- }
- }
|