From eae94e3dbfb477b3c79e706f402d30c93b7aad47 Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Mon, 31 Jan 2022 04:45:16 +0530 Subject: [PATCH] refactor: migration teams adapters to urql --- .../helpers/backend/GQLClient.ts | 105 +++- .../gql/queries/GetCollectionChildren.graphql | 8 + .../gql/queries/GetCollectionRequests.graphql | 7 + .../gql/queries/GetTeamMembers.graphql | 12 + .../gql/queries/RootCollectionsOfTeam.graphql | 6 + .../subscriptions/TeamCollectionAdded.graphql | 9 + .../TeamCollectionRemoved.graphql | 3 + .../TeamCollectionUpdated.graphql | 9 + .../gql/subscriptions/TeamMemberAdded.graphql | 5 + .../subscriptions/TeamMemberUpdated.graphql | 5 + .../subscriptions/TeamRequestAdded.graphql | 8 + .../subscriptions/TeamRequestDeleted.graphql | 3 + .../subscriptions/TeamRequestUpdated.graphql | 8 + .../helpers/teams/TeamCollectionAdapter.ts | 546 +++++++++--------- .../helpers/teams/TeamMemberAdapter.ts | 166 +++--- 15 files changed, 535 insertions(+), 365 deletions(-) create mode 100644 packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionChildren.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionRequests.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/queries/GetTeamMembers.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/queries/RootCollectionsOfTeam.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionAdded.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionRemoved.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionUpdated.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestAdded.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestDeleted.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestUpdated.graphql diff --git a/packages/hoppscotch-app/helpers/backend/GQLClient.ts b/packages/hoppscotch-app/helpers/backend/GQLClient.ts index d2e11d468..d779c7162 100644 --- a/packages/hoppscotch-app/helpers/backend/GQLClient.ts +++ b/packages/hoppscotch-app/helpers/backend/GQLClient.ts @@ -30,6 +30,7 @@ import * as E from "fp-ts/Either" import * as TE from "fp-ts/TaskEither" import { pipe, constVoid } from "fp-ts/function" import { Source, subscribe, pipe as wonkaPipe, onEnd } from "wonka" +import { Subject } from "rxjs" import { keyDefs } from "./caching/keys" import { optimisticDefs } from "./caching/optimistic" import { updatesDef } from "./caching/updates" @@ -122,7 +123,6 @@ const createHoppClient = () => fetchExchange, subscriptionExchange({ forwardSubscription: (operation) => - // @ts-expect-error: An issue with the Urql typing subscriptionClient.request(operation), }), ], @@ -145,6 +145,11 @@ type UseQueryOptions = { pollDuration?: number | undefined } +type RunQueryOptions = { + query: TypedDocumentNode + variables?: V +} + /** * A wrapper type for defining errors possible in a GQL operation */ @@ -158,6 +163,104 @@ export type GQLError = error: T } +export const runGQLQuery = ( + args: RunQueryOptions +): Promise, DocType>> => { + const request = createRequest(args.query, args.variables) + const source = client.value.executeQuery(request) + + return new Promise((resolve) => { + const sub = wonkaPipe( + source, + subscribe((res) => { + if (sub) { + sub.unsubscribe() + } + + pipe( + // The target + res.data as DocType | undefined, + // Define what happens if data does not exist (it is an error) + E.fromNullable( + pipe( + // Take the network error value + res.error?.networkError, + // If it null, set the left to the generic error name + E.fromNullable(res.error?.message), + E.match( + // The left case (network error was null) + (gqlErr) => + >{ + type: "gql_error", + error: parseGQLErrorString(gqlErr ?? "") as DocErrorType, + }, + // The right case (it was a GraphQL Error) + (networkErr) => + >{ + type: "network_error", + error: networkErr, + } + ) + ) + ), + resolve + ) + }) + ) + }) +} + +export const runGQLSubscription = < + DocType, + DocVarType, + DocErrorType extends string +>( + args: RunQueryOptions +) => { + const result$ = new Subject, DocType>>() + + const source = client.value.executeSubscription( + createRequest(args.query, args.variables) + ) + + wonkaPipe( + source, + subscribe((res) => { + result$.next( + pipe( + // The target + res.data as DocType | undefined, + // Define what happens if data does not exist (it is an error) + E.fromNullable( + pipe( + // Take the network error value + res.error?.networkError, + // If it null, set the left to the generic error name + E.fromNullable(res.error?.message), + E.match( + // The left case (network error was null) + (gqlErr) => + >{ + type: "gql_error", + error: parseGQLErrorString(gqlErr ?? "") as DocErrorType, + }, + // The right case (it was a GraphQL Error) + (networkErr) => + >{ + type: "network_error", + error: networkErr, + } + ) + ) + ) + ) + ) + }) + ) + + return result$ +} + export const useGQLQuery = ( _args: UseQueryOptions ) => { diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionChildren.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionChildren.graphql new file mode 100644 index 000000000..22218f772 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionChildren.graphql @@ -0,0 +1,8 @@ +query GetCollectionChildren($collectionID: ID!) { + collection(collectionID: $collectionID) { + children { + id + title + } + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionRequests.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionRequests.graphql new file mode 100644 index 000000000..e5d0b559b --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/GetCollectionRequests.graphql @@ -0,0 +1,7 @@ +query GetCollectionRequests($collectionID: ID!, $cursor: ID) { + requestsInCollection(collectionID: $collectionID, cursor: $cursor) { + id + title + request + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/GetTeamMembers.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/GetTeamMembers.graphql new file mode 100644 index 000000000..d6fa4f041 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/GetTeamMembers.graphql @@ -0,0 +1,12 @@ +query GetTeamMembers($teamID: ID!, $cursor: ID) { + team(teamID: $teamID) { + members(cursor: $cursor) { + membershipID + user { + uid + email + } + role + } + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/RootCollectionsOfTeam.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/RootCollectionsOfTeam.graphql new file mode 100644 index 000000000..f90041c54 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/RootCollectionsOfTeam.graphql @@ -0,0 +1,6 @@ +query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) { + rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) { + id + title + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionAdded.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionAdded.graphql new file mode 100644 index 000000000..70c9c3705 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionAdded.graphql @@ -0,0 +1,9 @@ +subscription TeamCollectionAdded($teamID: ID!) { + teamCollectionAdded(teamID: $teamID) { + id + title + parent { + id + } + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionRemoved.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionRemoved.graphql new file mode 100644 index 000000000..e9d9431d9 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionRemoved.graphql @@ -0,0 +1,3 @@ +subscription TeamCollectionRemoved($teamID: ID!) { + teamCollectionRemoved(teamID: $teamID) +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionUpdated.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionUpdated.graphql new file mode 100644 index 000000000..94f42eda9 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamCollectionUpdated.graphql @@ -0,0 +1,9 @@ +subscription TeamCollectionUpdated($teamID: ID!) { + teamCollectionUpdated(teamID: $teamID) { + id + title + parent { + id + } + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberAdded.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberAdded.graphql index dd15795e0..79b719934 100644 --- a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberAdded.graphql +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberAdded.graphql @@ -1,5 +1,10 @@ subscription TeamMemberAdded($teamID: ID!) { teamMemberAdded(teamID: $teamID) { membershipID + user { + uid + email + } + role } } diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberUpdated.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberUpdated.graphql index bed609224..bdf0e52c1 100644 --- a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberUpdated.graphql +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamMemberUpdated.graphql @@ -1,5 +1,10 @@ subscription TeamMemberUpdated($teamID: ID!) { teamMemberUpdated(teamID: $teamID) { membershipID + user { + uid + email + } + role } } diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestAdded.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestAdded.graphql new file mode 100644 index 000000000..d00b6a7a8 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestAdded.graphql @@ -0,0 +1,8 @@ +subscription TeamRequestAdded($teamID: ID!) { + teamRequestAdded(teamID: $teamID) { + id + collectionID + request + title + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestDeleted.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestDeleted.graphql new file mode 100644 index 000000000..8045e83f3 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestDeleted.graphql @@ -0,0 +1,3 @@ +subscription TeamRequestDeleted($teamID: ID!) { + teamRequestDeleted(teamID: $teamID) +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestUpdated.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestUpdated.graphql new file mode 100644 index 000000000..ef099da52 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/TeamRequestUpdated.graphql @@ -0,0 +1,8 @@ +subscription TeamRequestUpdated($teamID: ID!) { + teamRequestUpdated(teamID: $teamID) { + id + collectionID + request + title + } +} diff --git a/packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts b/packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts index 76e964211..f23c9881f 100644 --- a/packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts +++ b/packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts @@ -1,24 +1,24 @@ -import { BehaviorSubject } from "rxjs" -import { gql } from "graphql-tag" +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 { translateToNewRequest } from "@hoppscotch/data" +import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient" import { TeamCollection } from "./TeamCollection" import { TeamRequest } from "./TeamRequest" import { - rootCollectionsOfTeam, - getCollectionChildren, - getCollectionRequests, -} from "./utils" -import { apolloClient } from "~/helpers/apollo" + RootCollectionsOfTeamDocument, + TeamCollectionAddedDocument, + TeamCollectionUpdatedDocument, + TeamCollectionRemovedDocument, + TeamRequestAddedDocument, + TeamRequestUpdatedDocument, + TeamRequestDeletedDocument, + GetCollectionChildrenDocument, + GetCollectionRequestsDocument, +} from "~/helpers/backend/graphql" -/* - * NOTE: These functions deal with REFERENCES to objects and mutates them, for a simpler implementation. - * Be careful when you play with these. - * - * I am not a fan of mutating references but this is so much simpler compared to mutating clones - * - Andrew - */ +const TEAMS_BACKEND_PAGE_SIZE = 10 /** * Finds the parent of a collection and returns the REFERENCE (or null) @@ -76,32 +76,49 @@ function findCollInTree( } /** - * Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null) + * Deletes a collection in the tree * - * @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 + * @param {TeamCollection[]} tree - The tree to delete in (THIS WILL BE MUTATED!) + * @param {string} targetID - ID of the collection to delete */ -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 - } +function deleteCollInTree(tree: TeamCollection[], targetID: string) { + // Get the parent owning the collection + const parent = findParentOfColl(tree, targetID) - // Check in children of collections - if (coll.children) { - const result = findCollWithReqIDInTree(coll.children, reqID) - if (result) return result - } + // If we found a parent, update it + if (parent && parent.children) { + parent.children = parent.children.filter((coll) => coll.id !== targetID) } - // No matches - return null + // 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 & Pick} 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 & Pick +) { + const el = findCollInTree(tree, updateColl.id) + + // If no match, stop the operation + if (!el) return + + // Update all the specified keys + Object.assign(el, updateColl) } /** @@ -135,78 +152,47 @@ function findReqInTree( } /** - * Updates a collection in the tree with the specified data + * Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null) * - * @param {TeamCollection[]} tree - The tree to update in (THIS WILL BE MUTATED!) - * @param {Partial & Pick} updateColl - An object defining all the fields that should be updated (ID is required to find the target collection) + * @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 updateCollInTree( +function findCollWithReqIDInTree( tree: TeamCollection[], - updateColl: Partial & Pick -) { - const el = findCollInTree(tree, updateColl.id) + 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 + } - // If no match, stop the operation - if (!el) return - - // Update all the specified keys - Object.assign(el, updateColl) -} - -/** - * 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) + // Check in children of collections + if (coll.children) { + const result = findCollWithReqIDInTree(coll.children, reqID) + if (result) return result + } } - // 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) + // No matches + return null } -/** - * TeamCollectionAdapter provides a reactive collections list for a specific team - */ -export default class TeamCollectionAdapter { - /** - * The reactive list of collections - * - * A new value is emitted when there is a change - * (Use views instead) - */ +export default class NewTeamCollectionAdapter { collections$: BehaviorSubject - // Fields for subscriptions, used for destroying once not needed - private teamCollectionAdded$: ZenObservable.Subscription | null - private teamCollectionUpdated$: ZenObservable.Subscription | null - private teamCollectionRemoved$: ZenObservable.Subscription | null - private teamRequestAdded$: ZenObservable.Subscription | null - private teamRequestUpdated$: ZenObservable.Subscription | null - private teamRequestDeleted$: ZenObservable.Subscription | null + private teamCollectionAdded$: Subscription | null + private teamCollectionUpdated$: Subscription | null + private teamCollectionRemoved$: Subscription | null + private teamRequestAdded$: Subscription | null + private teamRequestUpdated$: Subscription | null + private teamRequestDeleted$: Subscription | null - /** - * @constructor - * - * @param {string | null} teamID - ID of the team to listen to, or null if none decided and the adapter should stand by - */ constructor(private teamID: string | null) { this.collections$ = new BehaviorSubject([]) + this.teamCollectionAdded$ = null this.teamCollectionUpdated$ = null this.teamCollectionRemoved$ = null @@ -217,15 +203,11 @@ export default class TeamCollectionAdapter { if (this.teamID) this.initialize() } - /** - * Updates the team the adapter is looking at - * - * @param {string | null} newTeamID - ID of the team to listen to, or null if none decided and the adapter should stand by - */ changeTeamID(newTeamID: string | null) { + this.teamID = newTeamID this.collections$.next([]) - this.teamID = newTeamID + this.unsubscribeSubscriptions() if (this.teamID) this.initialize() } @@ -243,22 +225,11 @@ export default class TeamCollectionAdapter { this.teamRequestUpdated$?.unsubscribe() } - /** - * Initializes the adapter - */ private async initialize() { await this.loadRootCollections() this.registerSubscriptions() } - /** - * Loads the root collections - */ - private async loadRootCollections(): Promise { - const colls = await rootCollectionsOfTeam(apolloClient, this.teamID) - this.collections$.next(colls) - } - /** * Performs addition of a collection to the tree * @@ -288,6 +259,44 @@ export default class TeamCollectionAdapter { this.collections$.next(tree) } + private async loadRootCollections() { + if (this.teamID === null) throw new Error("Team ID is null") + + 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)) + throw new Error(`Error fetching root collections: ${result}`) + + totalCollections.push( + ...result.right.rootCollectionsOfTeam.map( + (x) => + { + ...x, + children: null, + requests: null, + } + ) + ) + + if (result.right.rootCollectionsOfTeam.length !== TEAMS_BACKEND_PAGE_SIZE) + break + } + + this.collections$.next(totalCollections) + } + /** * Updates an existing collection in tree * @@ -337,25 +346,6 @@ export default class TeamCollectionAdapter { 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) - } - /** * Updates the request in tree * @@ -376,143 +366,121 @@ export default class TeamCollectionAdapter { } /** - * Registers the subscriptions to listen to team collection updates + * Removes a request from the tree + * + * @param {string} requestID - ID of the request to remove */ - registerSubscriptions() { - this.teamCollectionAdded$ = apolloClient - .subscribe({ - query: gql` - subscription TeamCollectionAdded($teamID: ID!) { - teamCollectionAdded(teamID: $teamID) { - id - title - parent { - id - } - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.addCollection( - { - id: data.teamCollectionAdded.id, - children: null, - requests: null, - title: data.teamCollectionAdded.title, - }, - data.teamCollectionAdded.parent?.id - ) - }) + private removeRequest(requestID: string) { + const tree = this.collections$.value - this.teamCollectionUpdated$ = apolloClient - .subscribe({ - query: gql` - subscription TeamCollectionUpdated($teamID: ID!) { - teamCollectionUpdated(teamID: $teamID) { - id - title - parent { - id - } - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.updateCollection({ - id: data.teamCollectionUpdated.id, - title: data.teamCollectionUpdated.title, - }) - }) + // Find request in tree, don't attempt if no collection or no requests (expansion?) + const coll = findCollWithReqIDInTree(tree, requestID) + if (!coll || !coll.requests) return - this.teamCollectionRemoved$ = apolloClient - .subscribe({ - query: gql` - subscription TeamCollectionRemoved($teamID: ID!) { - teamCollectionRemoved(teamID: $teamID) - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.removeCollection(data.teamCollectionRemoved) - }) + // Remove the collection + remove(coll.requests, (req) => req.id === requestID) - this.teamRequestAdded$ = apolloClient - .subscribe({ - query: gql` - subscription TeamRequestAdded($teamID: ID!) { - teamRequestAdded(teamID: $teamID) { - id - collectionID - request - title - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.addRequest({ - id: data.teamRequestAdded.id, - collectionID: data.teamRequestAdded.collectionID, - request: translateToNewRequest( - JSON.parse(data.teamRequestAdded.request) - ), - title: data.teamRequestAdded.title, - }) - }) + // Publish new tree + this.collections$.next(tree) + } - this.teamRequestUpdated$ = apolloClient - .subscribe({ - query: gql` - subscription TeamRequestUpdated($teamID: ID!) { - teamRequestUpdated(teamID: $teamID) { - id - collectionID - request - title - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.updateRequest({ - id: data.teamRequestUpdated.id, - collectionID: data.teamRequestUpdated.collectionID, - request: JSON.parse(data.teamRequestUpdated.request), - title: data.teamRequestUpdated.title, - }) - }) + private registerSubscriptions() { + if (!this.teamID) return - this.teamRequestDeleted$ = apolloClient - .subscribe({ - query: gql` - subscription TeamRequestDeleted($teamID: ID!) { - teamRequestDeleted(teamID: $teamID) - } - `, - variables: { - teamID: this.teamID, + this.teamCollectionAdded$ = runGQLSubscription({ + query: TeamCollectionAddedDocument, + variables: { + teamID: this.teamID, + }, + }).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 + ) + }) + + this.teamCollectionUpdated$ = runGQLSubscription({ + query: TeamCollectionUpdatedDocument, + variables: { + teamID: this.teamID, + }, + }).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, }) - .subscribe(({ data }) => { - this.removeRequest(data.teamRequestDeleted) + }) + + this.teamCollectionRemoved$ = runGQLSubscription({ + query: TeamCollectionRemovedDocument, + variables: { + teamID: this.teamID, + }, + }).subscribe((result) => { + if (E.isLeft(result)) + throw new Error(`Team Collection Removed Error: ${result.left}`) + + this.removeCollection(result.right.teamCollectionRemoved) + }) + + this.teamRequestAdded$ = runGQLSubscription({ + query: TeamRequestAddedDocument, + variables: { + teamID: this.teamID, + }, + }).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, }) + }) + + this.teamRequestUpdated$ = runGQLSubscription({ + query: TeamRequestUpdatedDocument, + variables: { + teamID: this.teamID, + }, + }).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, + }) + }) + + this.teamRequestDeleted$ = runGQLSubscription({ + query: TeamRequestDeletedDocument, + variables: { + teamID: this.teamID, + }, + }).subscribe((result) => { + if (E.isLeft(result)) + throw new Error(`Team Request Deleted Error ${result.left}`) + + this.removeRequest(result.right.teamRequestDeleted) + }) } /** @@ -533,28 +501,54 @@ export default class TeamCollectionAdapter { if (collection.children != null) return - const collections: TeamCollection[] = ( - await getCollectionChildren(apolloClient, collectionID) - ).map((el) => { - return { - id: el.id, - title: el.title, - children: null, - requests: null, - } + // TODO: Implement deep pagination + const collectionData = await runGQLQuery({ + query: GetCollectionChildrenDocument, + variables: { + collectionID, + }, }) - const requests: TeamRequest[] = ( - await getCollectionRequests(apolloClient, collectionID) - ).map((el) => { - return { - id: el.id, + if (E.isLeft(collectionData)) + throw new Error( + `Child Collection Fetch Error for ${collectionID}: ${collectionData.left}` + ) + + const collections: TeamCollection[] = + collectionData.right.collection?.children.map( + (el) => + { + id: el.id, + title: el.title, + children: null, + requests: null, + } + ) ?? [] + + // TODO: Implement deep pagination + const requestData = await runGQLQuery({ + query: GetCollectionRequestsDocument, + variables: { collectionID, - title: el.title, - request: translateToNewRequest(JSON.parse(el.request)), - } + cursor: undefined, + }, }) + if (E.isLeft(requestData)) + throw new Error( + `Child Request Fetch Error for ${requestData}: ${requestData.left}` + ) + + const requests: TeamRequest[] = + requestData.right.requestsInCollection.map((el) => { + return { + id: el.id, + collectionID, + title: el.title, + request: translateToNewRequest(JSON.parse(el.request)), + } + }) + collection.children = collections collection.requests = requests diff --git a/packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts b/packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts index ec35118b7..69c280d74 100644 --- a/packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts +++ b/packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts @@ -1,14 +1,19 @@ -import { BehaviorSubject } from "rxjs" -import gql from "graphql-tag" +import * as E from "fp-ts/Either" +import { BehaviorSubject, Subscription } from "rxjs" import cloneDeep from "lodash/cloneDeep" -import * as Apollo from "@apollo/client/core" -import { apolloClient } from "~/helpers/apollo" +import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient" +import { + GetTeamMembersDocument, + TeamMemberAddedDocument, + TeamMemberRemovedDocument, + TeamMemberUpdatedDocument, +} from "../backend/graphql" export interface TeamsTeamMember { membershipID: string user: { uid: string - email: string + email: string | null } role: "OWNER" | "EDITOR" | "VIEWER" } @@ -16,9 +21,9 @@ export interface TeamsTeamMember { export default class TeamMemberAdapter { members$: BehaviorSubject - private teamMemberAdded$: ZenObservable.Subscription | null - private teamMemberRemoved$: ZenObservable.Subscription | null - private teamMemberUpdated$: ZenObservable.Subscription | null + private teamMemberAdded$: Subscription | null + private teamMemberRemoved$: Subscription | null + private teamMemberUpdated$: Subscription | null constructor(private teamID: string | null) { this.members$ = new BehaviorSubject([]) @@ -50,37 +55,31 @@ export default class TeamMemberAdapter { } private async loadTeamMembers(): Promise { + if (!this.teamID) return + const result: TeamsTeamMember[] = [] let cursor: string | null = null + while (true) { - const response: Apollo.ApolloQueryResult = await apolloClient.query({ - query: gql` - query GetTeamMembers($teamID: ID!, $cursor: ID) { - team(teamID: $teamID) { - members(cursor: $cursor) { - membershipID - user { - uid - email - } - role - } - } - } - `, + const res = await runGQLQuery({ + query: GetTeamMembersDocument, variables: { teamID: this.teamID, cursor, }, }) - result.push(...response.data.team.members) + if (E.isLeft(res)) + throw new Error(`Team Members List Load failed: ${res.left}`) - if ((response.data.team.members as any[]).length === 0) break + // TODO: Improve this with TypeScript + result.push(...(res.right.team!.members as any)) + + if ((res.right.team!.members as any[]).length === 0) break else { cursor = - response.data.team.members[response.data.team.members.length - 1] + res.right.team!.members[res.right.team!.members.length - 1] .membershipID } } @@ -89,72 +88,63 @@ export default class TeamMemberAdapter { } private registerSubscriptions() { - this.teamMemberAdded$ = apolloClient - .subscribe({ - query: gql` - subscription TeamMemberAdded($teamID: ID!) { - teamMemberAdded(teamID: $teamID) { - user { - uid - email - } - role - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.members$.next([...this.members$.value, data.teamMemberAdded]) - }) + if (!this.teamID) return - this.teamMemberRemoved$ = apolloClient - .subscribe({ - query: gql` - subscription TeamMemberRemoved($teamID: ID!) { - teamMemberRemoved(teamID: $teamID) - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - this.members$.next( - this.members$.value.filter( - (el) => el.user.uid !== data.teamMemberRemoved - ) - ) - }) + this.teamMemberAdded$ = runGQLSubscription({ + query: TeamMemberAddedDocument, + variables: { + teamID: this.teamID, + }, + }).subscribe((result) => { + if (E.isLeft(result)) + throw new Error(`Team Member Added Subscription Failed: ${result.left}`) - this.teamMemberUpdated$ = apolloClient - .subscribe({ - query: gql` - subscription TeamMemberUpdated($teamID: ID!) { - teamMemberUpdated(teamID: $teamID) { - user { - uid - email - } - role - } - } - `, - variables: { - teamID: this.teamID, - }, - }) - .subscribe(({ data }) => { - const list = cloneDeep(this.members$.value) - const obj = list.find( - (el) => el.user.uid === data.teamMemberUpdated.user.uid + // TODO: Improve typing + this.members$.next([ + ...(this.members$.value as any), + result.right.teamMemberAdded as any, + ]) + }) + + this.teamMemberRemoved$ = runGQLSubscription({ + query: TeamMemberRemovedDocument, + variables: { + teamID: this.teamID, + }, + }).subscribe((result) => { + if (E.isLeft(result)) + throw new Error( + `Team Member Removed Subscription Failed: ${result.left}` ) - if (!obj) return + this.members$.next( + this.members$.value.filter( + (el) => el.user.uid !== result.right.teamMemberRemoved + ) + ) + }) - Object.assign(obj, data.teamMemberUpdated) - }) + this.teamMemberUpdated$ = runGQLSubscription({ + query: TeamMemberUpdatedDocument, + variables: { + teamID: this.teamID, + }, + }).subscribe((result) => { + if (E.isLeft(result)) + throw new Error( + `Team Member Updated Subscription Failed: ${result.left}` + ) + + const list = cloneDeep(this.members$.value) + // TODO: Improve typing situation + const obj = list.find( + (el) => + el.user.uid === (result.right.teamMemberUpdated.user!.uid as any) + ) + + if (!obj) return + + Object.assign(obj, result.right.teamMemberUpdated) + }) } }