chore: split app to commons and web (squash commit)
This commit is contained in:
388
packages/hoppscotch-common/src/helpers/backend/GQLClient.ts
Normal file
388
packages/hoppscotch-common/src/helpers/backend/GQLClient.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { ref } from "vue"
|
||||
import {
|
||||
createClient,
|
||||
TypedDocumentNode,
|
||||
dedupExchange,
|
||||
OperationContext,
|
||||
fetchExchange,
|
||||
makeOperation,
|
||||
createRequest,
|
||||
subscriptionExchange,
|
||||
errorExchange,
|
||||
CombinedError,
|
||||
Operation,
|
||||
OperationResult,
|
||||
} from "@urql/core"
|
||||
import { authExchange } from "@urql/exchange-auth"
|
||||
import { devtoolsExchange } from "@urql/devtools"
|
||||
import { SubscriptionClient } from "subscriptions-transport-ws"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe, constVoid, flow } from "fp-ts/function"
|
||||
import { subscribe, pipe as wonkaPipe } from "wonka"
|
||||
import { filter, map, Subject } from "rxjs"
|
||||
import {
|
||||
authIdToken$,
|
||||
getAuthIDToken,
|
||||
probableUser$,
|
||||
waitProbableLoginToConfirm,
|
||||
} from "~/helpers/fb/auth"
|
||||
|
||||
// TODO: Implement caching
|
||||
|
||||
const BACKEND_GQL_URL =
|
||||
import.meta.env.VITE_BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql"
|
||||
const BACKEND_WS_URL =
|
||||
import.meta.env.VITE_BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql"
|
||||
|
||||
type GQLOpType = "query" | "mutation" | "subscription"
|
||||
/**
|
||||
* A type that defines error events that are possible during backend operations on the GQLCLient
|
||||
*/
|
||||
export type GQLClientErrorEvent =
|
||||
| { type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT"; errors: Error[] }
|
||||
| { type: "CLIENT_REPORTED_ERROR"; error: CombinedError; op: Operation }
|
||||
| {
|
||||
type: "GQL_CLIENT_REPORTED_ERROR"
|
||||
opType: GQLOpType
|
||||
opResult: OperationResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A stream of the errors that occur during GQLClient operations.
|
||||
* Exposed to be subscribed to by systems like sentry for error reporting
|
||||
*/
|
||||
export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
|
||||
|
||||
const createSubscriptionClient = () => {
|
||||
return new SubscriptionClient(BACKEND_WS_URL, {
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authIdToken$.value}`,
|
||||
}
|
||||
},
|
||||
connectionCallback(error) {
|
||||
if (error?.length > 0) {
|
||||
gqlClientError$.next({
|
||||
type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT",
|
||||
errors: error,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createHoppClient = () => {
|
||||
const exchanges = [
|
||||
devtoolsExchange,
|
||||
dedupExchange,
|
||||
authExchange({
|
||||
addAuthToOperation({ authState, operation }) {
|
||||
if (!authState || !authState.authToken) {
|
||||
return operation
|
||||
}
|
||||
|
||||
const fetchOptions =
|
||||
typeof operation.context.fetchOptions === "function"
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {}
|
||||
|
||||
return makeOperation(operation.kind, operation, {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
willAuthError({ authState }) {
|
||||
return !authState || !authState.authToken
|
||||
},
|
||||
getAuth: async () => {
|
||||
if (!probableUser$.value) return { authToken: null }
|
||||
|
||||
await waitProbableLoginToConfirm()
|
||||
|
||||
return {
|
||||
authToken: getAuthIDToken(),
|
||||
}
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
errorExchange({
|
||||
onError(error, op) {
|
||||
gqlClientError$.next({
|
||||
type: "CLIENT_REPORTED_ERROR",
|
||||
error,
|
||||
op,
|
||||
})
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
if (subscriptionClient) {
|
||||
exchanges.push(
|
||||
subscriptionExchange({
|
||||
forwardSubscription: (operation) => {
|
||||
return subscriptionClient!.request(operation)
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return createClient({
|
||||
url: BACKEND_GQL_URL,
|
||||
exchanges,
|
||||
})
|
||||
}
|
||||
|
||||
let subscriptionClient: SubscriptionClient | null
|
||||
export const client = ref(createHoppClient())
|
||||
|
||||
authIdToken$.subscribe((idToken) => {
|
||||
// triggering reconnect by closing the websocket client
|
||||
if (idToken && subscriptionClient) {
|
||||
subscriptionClient?.client?.close()
|
||||
}
|
||||
|
||||
// creating new subscription
|
||||
if (idToken && !subscriptionClient) {
|
||||
subscriptionClient = createSubscriptionClient()
|
||||
}
|
||||
|
||||
// closing existing subscription client.
|
||||
if (!idToken && subscriptionClient) {
|
||||
subscriptionClient.close()
|
||||
subscriptionClient = null
|
||||
}
|
||||
|
||||
client.value = createHoppClient()
|
||||
})
|
||||
|
||||
type RunQueryOptions<T = any, V = object> = {
|
||||
query: TypedDocumentNode<T, V>
|
||||
variables?: V
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper type for defining errors possible in a GQL operation
|
||||
*/
|
||||
export type GQLError<T extends string> =
|
||||
| {
|
||||
type: "network_error"
|
||||
error: Error
|
||||
}
|
||||
| {
|
||||
type: "gql_error"
|
||||
error: T
|
||||
}
|
||||
|
||||
export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
||||
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
||||
const source = client.value.executeQuery(request, {
|
||||
requestPolicy: "network-only",
|
||||
})
|
||||
|
||||
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) => {
|
||||
if (res.error) {
|
||||
gqlClientError$.next({
|
||||
type: "GQL_CLIENT_REPORTED_ERROR",
|
||||
opType: "query",
|
||||
opResult: res,
|
||||
})
|
||||
}
|
||||
|
||||
return <GQLError<DocErrorType>>{
|
||||
type: "gql_error",
|
||||
error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
|
||||
}
|
||||
},
|
||||
// The right case (it was a GraphQL Error)
|
||||
(networkErr) =>
|
||||
<GQLError<DocErrorType>>{
|
||||
type: "network_error",
|
||||
error: networkErr,
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
resolve
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: The subscription system seems to be firing multiple updates for certain subscriptions.
|
||||
// Make sure to handle cases if the subscription fires with the same update multiple times
|
||||
export const runGQLSubscription = <
|
||||
DocType,
|
||||
DocVarType,
|
||||
DocErrorType extends string
|
||||
>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
) => {
|
||||
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
|
||||
|
||||
const source = client.value.executeSubscription(
|
||||
createRequest(args.query, args.variables)
|
||||
)
|
||||
|
||||
const sub = 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) => {
|
||||
if (res.error) {
|
||||
gqlClientError$.next({
|
||||
type: "GQL_CLIENT_REPORTED_ERROR",
|
||||
opType: "subscription",
|
||||
opResult: res,
|
||||
})
|
||||
}
|
||||
|
||||
return <GQLError<DocErrorType>>{
|
||||
type: "gql_error",
|
||||
error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
|
||||
}
|
||||
},
|
||||
// The right case (it was a GraphQL Error)
|
||||
(networkErr) =>
|
||||
<GQLError<DocErrorType>>{
|
||||
type: "network_error",
|
||||
error: networkErr,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// Returns the stream and a subscription handle to unsub
|
||||
return [result$, sub] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `runGQLSubscription` but stops the subscription silently
|
||||
* if there is an authentication error because of logged out
|
||||
*/
|
||||
export const runAuthOnlyGQLSubscription = flow(
|
||||
runGQLSubscription,
|
||||
([result$, sub]) => {
|
||||
const updatedResult$ = result$.pipe(
|
||||
map((res) => {
|
||||
if (
|
||||
E.isLeft(res) &&
|
||||
res.left.type === "gql_error" &&
|
||||
res.left.error === "auth/fail"
|
||||
) {
|
||||
sub.unsubscribe()
|
||||
return null
|
||||
} else return res
|
||||
}),
|
||||
filter((res): res is Exclude<typeof res, null> => res !== null)
|
||||
)
|
||||
|
||||
return [updatedResult$, sub] as const
|
||||
}
|
||||
)
|
||||
|
||||
export const parseGQLErrorString = (s: string) =>
|
||||
s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s
|
||||
|
||||
export const runMutation = <
|
||||
DocType,
|
||||
DocVariables extends object | undefined,
|
||||
DocErrors extends string
|
||||
>(
|
||||
mutation: TypedDocumentNode<DocType, DocVariables>,
|
||||
variables?: DocVariables,
|
||||
additionalConfig?: Partial<OperationContext>
|
||||
): TE.TaskEither<GQLError<DocErrors>, DocType> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
client.value
|
||||
.mutation(mutation, variables, {
|
||||
requestPolicy: "cache-and-network",
|
||||
...additionalConfig,
|
||||
})
|
||||
.toPromise(),
|
||||
() => constVoid() as never // The mutation function can never fail, so this will never be called ;)
|
||||
),
|
||||
TE.chainEitherK((result) =>
|
||||
pipe(
|
||||
result.data,
|
||||
E.fromNullable(
|
||||
// Result is null
|
||||
pipe(
|
||||
result.error?.networkError,
|
||||
E.fromNullable(result.error?.message),
|
||||
E.match(
|
||||
// The left case (network error was null)
|
||||
(gqlErr) => {
|
||||
if (result.error) {
|
||||
gqlClientError$.next({
|
||||
type: "GQL_CLIENT_REPORTED_ERROR",
|
||||
opType: "mutation",
|
||||
opResult: result,
|
||||
})
|
||||
}
|
||||
|
||||
return <GQLError<DocErrors>>{
|
||||
type: "gql_error",
|
||||
error: parseGQLErrorString(gqlErr ?? ""),
|
||||
}
|
||||
},
|
||||
// The right case (it was a network error)
|
||||
(networkErr) =>
|
||||
<GQLError<DocErrors>>{
|
||||
type: "network_error",
|
||||
error: networkErr,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
export type UserQueryError = "user/not_found"
|
||||
|
||||
export type MyTeamsQueryError = "ea/not_invite_or_admin"
|
||||
@@ -0,0 +1,12 @@
|
||||
mutation AcceptTeamInvitation($inviteID: ID!) {
|
||||
acceptTeamInvitation(inviteID: $inviteID) {
|
||||
membershipID
|
||||
role
|
||||
user {
|
||||
uid
|
||||
displayName
|
||||
photoURL
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mutation CreateChildCollection(
|
||||
$childTitle: String!
|
||||
$collectionID: ID!
|
||||
) {
|
||||
createChildCollection(
|
||||
childTitle: $childTitle
|
||||
collectionID: $collectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mutation CreateDuplicateEnvironment($id: ID!){
|
||||
createDuplicateEnvironment (id: $id ){
|
||||
id
|
||||
teamID
|
||||
name
|
||||
variables
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateNewRootCollection($title: String!, $teamID: ID!) {
|
||||
createRootCollection(title: $title, teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mutation CreateRequestInCollection($data: CreateTeamRequestInput!, $collectionID: ID!) {
|
||||
createRequestInCollection(data: $data, collectionID: $collectionID) {
|
||||
id
|
||||
collection {
|
||||
id
|
||||
team {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation CreateShortcode($request: String!) {
|
||||
createShortcode(request: $request) {
|
||||
id
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
mutation CreateTeam($name: String!) {
|
||||
createTeam(name: $name) {
|
||||
id
|
||||
name
|
||||
members {
|
||||
membershipID
|
||||
role
|
||||
user {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
photoURL
|
||||
}
|
||||
}
|
||||
myRole
|
||||
ownersCount
|
||||
editorsCount
|
||||
viewersCount
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
|
||||
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
|
||||
variables
|
||||
name
|
||||
teamID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation CreateTeamInvitation($inviteeEmail: String!, $inviteeRole: TeamMemberRole!, $teamID: ID!) {
|
||||
createTeamInvitation(inviteeRole: $inviteeRole, inviteeEmail: $inviteeEmail, teamID: $teamID) {
|
||||
id
|
||||
teamID
|
||||
creatorUid
|
||||
inviteeEmail
|
||||
inviteeRole
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteCollection($collectionID: ID!) {
|
||||
deleteCollection(collectionID: $collectionID)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteRequest($requestID: ID!) {
|
||||
deleteRequest(requestID: $requestID)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteShortcode($code: ID!) {
|
||||
revokeShortcode(code: $code)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteTeam($teamID: ID!) {
|
||||
deleteTeam(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteTeamEnvironment($id: ID!){
|
||||
deleteTeamEnvironment (id: $id )
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation importFromJSON($jsonString: String!, $teamID: ID!) {
|
||||
importCollectionsFromJSON(jsonString: $jsonString, teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation LeaveTeam($teamID: ID!) {
|
||||
leaveTeam(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation MoveRESTTeamRequest($requestID: ID!, $collectionID: ID!) {
|
||||
moveRequest(requestID: $requestID, destCollID: $collectionID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) {
|
||||
removeTeamMember(userUid: $userUid, teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation RenameCollection($newTitle: String!, $collectionID: ID!) {
|
||||
renameCollection(newTitle: $newTitle, collectionID: $collectionID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation RenameTeam($newName: String!, $teamID: ID!) {
|
||||
renameTeam(newName: $newName, teamID: $teamID) {
|
||||
id
|
||||
name
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation RevokeTeamInvitation($inviteID: ID!) {
|
||||
revokeTeamInvitation(inviteID: $inviteID)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation UpdateRequest($data: UpdateTeamRequestInput!, $requestID: ID!) {
|
||||
updateRequest(data: $data, requestID: $requestID) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation UpdateTeamEnvironment($variables: String!,$id: ID!,$name: String!){
|
||||
updateTeamEnvironment( variables: $variables ,id: $id ,name: $name){
|
||||
variables
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
mutation UpdateTeamMemberRole(
|
||||
$newRole: TeamMemberRole!,
|
||||
$userUid: ID!,
|
||||
$teamID: ID!
|
||||
) {
|
||||
updateTeamMemberRole(
|
||||
newRole: $newRole
|
||||
userUid: $userUid
|
||||
teamID: $teamID
|
||||
) {
|
||||
membershipID
|
||||
role
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
query ExportAsJSON($teamID: ID!) {
|
||||
exportCollectionsToJSON(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
query GetCollectionChildren($collectionID: ID!, $cursor: String) {
|
||||
collection(collectionID: $collectionID) {
|
||||
children(cursor: $cursor) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query GetCollectionChildrenIDs($collectionID: ID!, $cursor: String) {
|
||||
collection(collectionID: $collectionID) {
|
||||
children(cursor: $cursor) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query GetCollectionRequests($collectionID: ID!, $cursor: ID) {
|
||||
requestsInCollection(collectionID: $collectionID, cursor: $cursor) {
|
||||
id
|
||||
title
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query GetCollectionTitle($collectionID: ID!) {
|
||||
collection(collectionID: $collectionID) {
|
||||
title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
query GetInviteDetails($inviteID: ID!) {
|
||||
teamInvitation(inviteID: $inviteID) {
|
||||
id
|
||||
inviteeEmail
|
||||
inviteeRole
|
||||
team {
|
||||
id
|
||||
name
|
||||
}
|
||||
creator {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query GetUserShortcodes($cursor: ID) {
|
||||
myShortcodes(cursor: $cursor) {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
query GetMyTeams($cursor: ID) {
|
||||
myTeams(cursor: $cursor) {
|
||||
id
|
||||
name
|
||||
myRole
|
||||
ownersCount
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
photoURL
|
||||
displayName
|
||||
email
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query GetTeam($teamID: ID!) {
|
||||
team(teamID: $teamID) {
|
||||
id
|
||||
name
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
query GetTeamEnvironments($teamID: ID!){
|
||||
team(teamID: $teamID){
|
||||
teamEnvironments{
|
||||
id
|
||||
name
|
||||
variables
|
||||
teamID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query GetTeamMembers($teamID: ID!, $cursor: ID) {
|
||||
team(teamID: $teamID) {
|
||||
members(cursor: $cursor) {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
query GetUserInfo {
|
||||
me {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
photoURL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query Me {
|
||||
me {
|
||||
uid
|
||||
displayName
|
||||
photoURL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
query ResolveShortcode($code: ID!) {
|
||||
shortcode(code: $code) {
|
||||
id
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) {
|
||||
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
query GetPendingInvites($teamID: ID!) {
|
||||
team(teamID: $teamID) {
|
||||
id
|
||||
teamInvitations {
|
||||
inviteeRole
|
||||
inviteeEmail
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
subscription ShortcodeCreated {
|
||||
myShortcodesCreated {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription ShortcodeDeleted {
|
||||
myShortcodesRevoked {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
subscription TeamCollectionAdded($teamID: ID!) {
|
||||
teamCollectionAdded(teamID: $teamID) {
|
||||
id
|
||||
title
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
subscription TeamCollectionRemoved($teamID: ID!) {
|
||||
teamCollectionRemoved(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
subscription TeamCollectionUpdated($teamID: ID!) {
|
||||
teamCollectionUpdated(teamID: $teamID) {
|
||||
id
|
||||
title
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
subscription TeamEnvironmentCreated ($teamID: ID!) {
|
||||
teamEnvironmentCreated(teamID: $teamID) {
|
||||
id
|
||||
teamID
|
||||
name
|
||||
variables
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription TeamEnvironmentDeleted ($teamID: ID!) {
|
||||
teamEnvironmentDeleted(teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
subscription TeamEnvironmentUpdated ($teamID: ID!) {
|
||||
teamEnvironmentUpdated(teamID: $teamID) {
|
||||
id
|
||||
teamID
|
||||
name
|
||||
variables
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription TeamInvitationAdded($teamID: ID!) {
|
||||
teamInvitationAdded(teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
subscription TeamInvitationRemoved($teamID: ID!) {
|
||||
teamInvitationRemoved(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription TeamMemberAdded($teamID: ID!) {
|
||||
teamMemberAdded(teamID: $teamID) {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
subscription TeamMemberRemoved($teamID: ID!) {
|
||||
teamMemberRemoved(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription TeamMemberUpdated($teamID: ID!) {
|
||||
teamMemberUpdated(teamID: $teamID) {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
subscription TeamRequestAdded($teamID: ID!) {
|
||||
teamRequestAdded(teamID: $teamID) {
|
||||
id
|
||||
collectionID
|
||||
request
|
||||
title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
subscription TeamRequestDeleted($teamID: ID!) {
|
||||
teamRequestDeleted(teamID: $teamID)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
subscription TeamRequestUpdated($teamID: ID!) {
|
||||
teamRequestUpdated(teamID: $teamID) {
|
||||
id
|
||||
collectionID
|
||||
request
|
||||
title
|
||||
}
|
||||
}
|
||||
127
packages/hoppscotch-common/src/helpers/backend/helpers.ts
Normal file
127
packages/hoppscotch-common/src/helpers/backend/helpers.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
makeCollection,
|
||||
translateToNewRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { TeamCollection } from "../teams/TeamCollection"
|
||||
import { TeamRequest } from "../teams/TeamRequest"
|
||||
import { GQLError, runGQLQuery } from "./GQLClient"
|
||||
import {
|
||||
GetCollectionChildrenIDsDocument,
|
||||
GetCollectionRequestsDocument,
|
||||
GetCollectionTitleDocument,
|
||||
} from "./graphql"
|
||||
|
||||
export const BACKEND_PAGE_SIZE = 10
|
||||
|
||||
const getCollectionChildrenIDs = async (collID: string) => {
|
||||
const collsList: string[] = []
|
||||
|
||||
while (true) {
|
||||
const data = await runGQLQuery({
|
||||
query: GetCollectionChildrenIDsDocument,
|
||||
variables: {
|
||||
collectionID: collID,
|
||||
cursor:
|
||||
collsList.length > 0 ? collsList[collsList.length - 1] : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
return E.left(data.left)
|
||||
}
|
||||
|
||||
collsList.push(...data.right.collection!.children.map((x) => x.id))
|
||||
|
||||
if (data.right.collection!.children.length !== BACKEND_PAGE_SIZE) break
|
||||
}
|
||||
|
||||
return E.right(collsList)
|
||||
}
|
||||
|
||||
const getCollectionRequests = async (collID: string) => {
|
||||
const reqList: TeamRequest[] = []
|
||||
|
||||
while (true) {
|
||||
const data = await runGQLQuery({
|
||||
query: GetCollectionRequestsDocument,
|
||||
variables: {
|
||||
collectionID: collID,
|
||||
cursor: reqList.length > 0 ? reqList[reqList.length - 1].id : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
return E.left(data.left)
|
||||
}
|
||||
|
||||
reqList.push(
|
||||
...data.right.requestsInCollection.map(
|
||||
(x) =>
|
||||
<TeamRequest>{
|
||||
id: x.id,
|
||||
request: translateToNewRequest(JSON.parse(x.request)),
|
||||
collectionID: collID,
|
||||
title: x.title,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (data.right.requestsInCollection.length !== BACKEND_PAGE_SIZE) break
|
||||
}
|
||||
|
||||
return E.right(reqList)
|
||||
}
|
||||
|
||||
export const getCompleteCollectionTree = (
|
||||
collID: string
|
||||
): TE.TaskEither<GQLError<string>, TeamCollection> =>
|
||||
pipe(
|
||||
TE.Do,
|
||||
|
||||
TE.bind("title", () =>
|
||||
pipe(
|
||||
() =>
|
||||
runGQLQuery({
|
||||
query: GetCollectionTitleDocument,
|
||||
variables: {
|
||||
collectionID: collID,
|
||||
},
|
||||
}),
|
||||
TE.map((x) => x.collection!.title)
|
||||
)
|
||||
),
|
||||
TE.bind("children", () =>
|
||||
pipe(
|
||||
// TaskEither -> () => Promise<Either>
|
||||
() => getCollectionChildrenIDs(collID),
|
||||
TE.chain(flow(A.map(getCompleteCollectionTree), TE.sequenceArray))
|
||||
)
|
||||
),
|
||||
|
||||
TE.bind("requests", () => () => getCollectionRequests(collID)),
|
||||
|
||||
TE.map(
|
||||
({ title, children, requests }) =>
|
||||
<TeamCollection>{
|
||||
id: collID,
|
||||
children,
|
||||
requests,
|
||||
title,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export const teamCollToHoppRESTColl = (
|
||||
coll: TeamCollection
|
||||
): HoppCollection<HoppRESTRequest> =>
|
||||
makeCollection({
|
||||
name: coll.title,
|
||||
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
|
||||
requests: coll.requests?.map((x) => x.request) ?? [],
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
CreateShortcodeDocument,
|
||||
CreateShortcodeMutation,
|
||||
CreateShortcodeMutationVariables,
|
||||
DeleteShortcodeDocument,
|
||||
DeleteShortcodeMutation,
|
||||
DeleteShortcodeMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type DeleteShortcodeErrors = "shortcode/not_found"
|
||||
|
||||
export const createShortcode = (request: HoppRESTRequest) =>
|
||||
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
|
||||
CreateShortcodeDocument,
|
||||
{
|
||||
request: JSON.stringify(request),
|
||||
}
|
||||
)
|
||||
|
||||
export const deleteShortcode = (code: string) =>
|
||||
runMutation<
|
||||
DeleteShortcodeMutation,
|
||||
DeleteShortcodeMutationVariables,
|
||||
DeleteShortcodeErrors
|
||||
>(DeleteShortcodeDocument, {
|
||||
code,
|
||||
})
|
||||
132
packages/hoppscotch-common/src/helpers/backend/mutations/Team.ts
Normal file
132
packages/hoppscotch-common/src/helpers/backend/mutations/Team.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { runMutation } from "../GQLClient"
|
||||
import { TeamName } from "../types/TeamName"
|
||||
import {
|
||||
CreateTeamDocument,
|
||||
CreateTeamMutation,
|
||||
CreateTeamMutationVariables,
|
||||
DeleteTeamDocument,
|
||||
DeleteTeamMutation,
|
||||
DeleteTeamMutationVariables,
|
||||
LeaveTeamDocument,
|
||||
LeaveTeamMutation,
|
||||
LeaveTeamMutationVariables,
|
||||
RemoveTeamMemberDocument,
|
||||
RemoveTeamMemberMutation,
|
||||
RemoveTeamMemberMutationVariables,
|
||||
RenameTeamDocument,
|
||||
RenameTeamMutation,
|
||||
RenameTeamMutationVariables,
|
||||
TeamMemberRole,
|
||||
UpdateTeamMemberRoleDocument,
|
||||
UpdateTeamMemberRoleMutation,
|
||||
UpdateTeamMemberRoleMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type DeleteTeamErrors =
|
||||
| "team/not_required_role"
|
||||
| "team/invalid_id"
|
||||
| "team/member_not_found"
|
||||
| "ea/not_invite_or_admin"
|
||||
|
||||
type LeaveTeamErrors =
|
||||
| "team/invalid_id"
|
||||
| "team/member_not_found"
|
||||
| "ea/not_invite_or_admin"
|
||||
|
||||
type CreateTeamErrors = "team/name_invalid" | "ea/not_invite_or_admin"
|
||||
|
||||
type RenameTeamErrors =
|
||||
| "ea/not_invite_or_admin"
|
||||
| "team/invalid_id"
|
||||
| "team/not_required_role"
|
||||
|
||||
type UpdateTeamMemberRoleErrors =
|
||||
| "ea/not_invite_or_admin"
|
||||
| "team/invalid_id"
|
||||
| "team/not_required_role"
|
||||
|
||||
type RemoveTeamMemberErrors =
|
||||
| "ea/not_invite_or_admin"
|
||||
| "team/invalid_id"
|
||||
| "team/not_required_role"
|
||||
|
||||
export const createTeam = (name: TeamName) =>
|
||||
pipe(
|
||||
runMutation<
|
||||
CreateTeamMutation,
|
||||
CreateTeamMutationVariables,
|
||||
CreateTeamErrors
|
||||
>(CreateTeamDocument, {
|
||||
name,
|
||||
}),
|
||||
TE.map(({ createTeam }) => createTeam)
|
||||
)
|
||||
|
||||
export const deleteTeam = (teamID: string) =>
|
||||
runMutation<
|
||||
DeleteTeamMutation,
|
||||
DeleteTeamMutationVariables,
|
||||
DeleteTeamErrors
|
||||
>(
|
||||
DeleteTeamDocument,
|
||||
{
|
||||
teamID,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ["Team"],
|
||||
}
|
||||
)
|
||||
|
||||
export const leaveTeam = (teamID: string) =>
|
||||
runMutation<LeaveTeamMutation, LeaveTeamMutationVariables, LeaveTeamErrors>(
|
||||
LeaveTeamDocument,
|
||||
{
|
||||
teamID,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ["Team"],
|
||||
}
|
||||
)
|
||||
|
||||
export const renameTeam = (teamID: string, newName: TeamName) =>
|
||||
pipe(
|
||||
runMutation<
|
||||
RenameTeamMutation,
|
||||
RenameTeamMutationVariables,
|
||||
RenameTeamErrors
|
||||
>(RenameTeamDocument, {
|
||||
newName,
|
||||
teamID,
|
||||
}),
|
||||
TE.map(({ renameTeam }) => renameTeam)
|
||||
)
|
||||
|
||||
export const updateTeamMemberRole = (
|
||||
userUid: string,
|
||||
teamID: string,
|
||||
newRole: TeamMemberRole
|
||||
) =>
|
||||
pipe(
|
||||
runMutation<
|
||||
UpdateTeamMemberRoleMutation,
|
||||
UpdateTeamMemberRoleMutationVariables,
|
||||
UpdateTeamMemberRoleErrors
|
||||
>(UpdateTeamMemberRoleDocument, {
|
||||
newRole,
|
||||
userUid,
|
||||
teamID,
|
||||
}),
|
||||
TE.map(({ updateTeamMemberRole }) => updateTeamMemberRole)
|
||||
)
|
||||
|
||||
export const removeTeamMember = (userUid: string, teamID: string) =>
|
||||
runMutation<
|
||||
RemoveTeamMemberMutation,
|
||||
RemoveTeamMemberMutationVariables,
|
||||
RemoveTeamMemberErrors
|
||||
>(RemoveTeamMemberDocument, {
|
||||
userUid,
|
||||
teamID,
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
CreateDuplicateEnvironmentDocument,
|
||||
CreateDuplicateEnvironmentMutation,
|
||||
CreateDuplicateEnvironmentMutationVariables,
|
||||
CreateTeamEnvironmentDocument,
|
||||
CreateTeamEnvironmentMutation,
|
||||
CreateTeamEnvironmentMutationVariables,
|
||||
DeleteTeamEnvironmentDocument,
|
||||
DeleteTeamEnvironmentMutation,
|
||||
DeleteTeamEnvironmentMutationVariables,
|
||||
UpdateTeamEnvironmentDocument,
|
||||
UpdateTeamEnvironmentMutation,
|
||||
UpdateTeamEnvironmentMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type DeleteTeamEnvironmentError = "team_environment/not_found"
|
||||
|
||||
type UpdateTeamEnvironmentError = "team_environment/not_found"
|
||||
|
||||
type DuplicateTeamEnvironmentError = "team_environment/not_found"
|
||||
|
||||
export const createTeamEnvironment = (
|
||||
variables: string,
|
||||
teamID: string,
|
||||
name: string
|
||||
) =>
|
||||
runMutation<
|
||||
CreateTeamEnvironmentMutation,
|
||||
CreateTeamEnvironmentMutationVariables,
|
||||
""
|
||||
>(CreateTeamEnvironmentDocument, {
|
||||
variables,
|
||||
teamID,
|
||||
name,
|
||||
})
|
||||
|
||||
export const deleteTeamEnvironment = (id: string) =>
|
||||
runMutation<
|
||||
DeleteTeamEnvironmentMutation,
|
||||
DeleteTeamEnvironmentMutationVariables,
|
||||
DeleteTeamEnvironmentError
|
||||
>(DeleteTeamEnvironmentDocument, {
|
||||
id,
|
||||
})
|
||||
|
||||
export const updateTeamEnvironment = (
|
||||
variables: string,
|
||||
id: string,
|
||||
name: string
|
||||
) =>
|
||||
runMutation<
|
||||
UpdateTeamEnvironmentMutation,
|
||||
UpdateTeamEnvironmentMutationVariables,
|
||||
UpdateTeamEnvironmentError
|
||||
>(UpdateTeamEnvironmentDocument, {
|
||||
variables,
|
||||
id,
|
||||
name,
|
||||
})
|
||||
|
||||
export const createDuplicateEnvironment = (id: string) =>
|
||||
runMutation<
|
||||
CreateDuplicateEnvironmentMutation,
|
||||
CreateDuplicateEnvironmentMutationVariables,
|
||||
DuplicateTeamEnvironmentError
|
||||
>(CreateDuplicateEnvironmentDocument, {
|
||||
id,
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
AcceptTeamInvitationDocument,
|
||||
AcceptTeamInvitationMutation,
|
||||
AcceptTeamInvitationMutationVariables,
|
||||
CreateTeamInvitationDocument,
|
||||
CreateTeamInvitationMutation,
|
||||
CreateTeamInvitationMutationVariables,
|
||||
RevokeTeamInvitationDocument,
|
||||
RevokeTeamInvitationMutation,
|
||||
RevokeTeamInvitationMutationVariables,
|
||||
TeamMemberRole,
|
||||
} from "../graphql"
|
||||
import { Email } from "../types/Email"
|
||||
|
||||
export type CreateTeamInvitationErrors =
|
||||
| "invalid/email"
|
||||
| "team/invalid_id"
|
||||
| "team/member_not_found"
|
||||
| "team_invite/already_member"
|
||||
| "team_invite/member_has_invite"
|
||||
|
||||
type RevokeTeamInvitationErrors =
|
||||
| "team/not_required_role"
|
||||
| "team_invite/no_invite_found"
|
||||
|
||||
type AcceptTeamInvitationErrors =
|
||||
| "team_invite/no_invite_found"
|
||||
| "team_invite/already_member"
|
||||
| "team_invite/email_do_not_match"
|
||||
|
||||
export const createTeamInvitation = (
|
||||
inviteeEmail: Email,
|
||||
inviteeRole: TeamMemberRole,
|
||||
teamID: string
|
||||
) =>
|
||||
pipe(
|
||||
runMutation<
|
||||
CreateTeamInvitationMutation,
|
||||
CreateTeamInvitationMutationVariables,
|
||||
CreateTeamInvitationErrors
|
||||
>(CreateTeamInvitationDocument, {
|
||||
inviteeEmail,
|
||||
inviteeRole,
|
||||
teamID,
|
||||
}),
|
||||
TE.map((x) => x.createTeamInvitation)
|
||||
)
|
||||
|
||||
export const revokeTeamInvitation = (inviteID: string) =>
|
||||
runMutation<
|
||||
RevokeTeamInvitationMutation,
|
||||
RevokeTeamInvitationMutationVariables,
|
||||
RevokeTeamInvitationErrors
|
||||
>(RevokeTeamInvitationDocument, {
|
||||
inviteID,
|
||||
})
|
||||
|
||||
export const acceptTeamInvitation = (inviteID: string) =>
|
||||
runMutation<
|
||||
AcceptTeamInvitationMutation,
|
||||
AcceptTeamInvitationMutationVariables,
|
||||
AcceptTeamInvitationErrors
|
||||
>(AcceptTeamInvitationDocument, {
|
||||
inviteID,
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
MoveRestTeamRequestDocument,
|
||||
MoveRestTeamRequestMutation,
|
||||
MoveRestTeamRequestMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type MoveRestTeamRequestErrors =
|
||||
| "team_req/not_found"
|
||||
| "team_req/invalid_target_id"
|
||||
|
||||
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
|
||||
runMutation<
|
||||
MoveRestTeamRequestMutation,
|
||||
MoveRestTeamRequestMutationVariables,
|
||||
MoveRestTeamRequestErrors
|
||||
>(MoveRestTeamRequestDocument, {
|
||||
requestID,
|
||||
collectionID,
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as t from "io-ts"
|
||||
|
||||
const emailRegex =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
interface EmailBrand {
|
||||
readonly Email: unique symbol
|
||||
}
|
||||
|
||||
export const EmailCodec = t.brand(
|
||||
t.string,
|
||||
(x): x is t.Branded<string, EmailBrand> => emailRegex.test(x),
|
||||
"Email"
|
||||
)
|
||||
|
||||
export type Email = t.TypeOf<typeof EmailCodec>
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as t from "io-ts"
|
||||
|
||||
interface TeamNameBrand {
|
||||
readonly TeamName: unique symbol
|
||||
}
|
||||
|
||||
export const TeamNameCodec = t.brand(
|
||||
t.string,
|
||||
(x): x is t.Branded<string, TeamNameBrand> => x.trim().length >= 6,
|
||||
"TeamName"
|
||||
)
|
||||
|
||||
export type TeamName = t.TypeOf<typeof TeamNameCodec>
|
||||
Reference in New Issue
Block a user