GQLClient.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {
  2. computed,
  3. ref,
  4. onMounted,
  5. onBeforeUnmount,
  6. reactive,
  7. Ref,
  8. } from "@nuxtjs/composition-api"
  9. import { DocumentNode } from "graphql/language"
  10. import {
  11. createClient,
  12. TypedDocumentNode,
  13. OperationResult,
  14. defaultExchanges,
  15. } from "@urql/core"
  16. import { devtoolsExchange } from "@urql/devtools"
  17. import * as E from "fp-ts/Either"
  18. import * as TE from "fp-ts/TaskEither"
  19. import { pipe, constVoid } from "fp-ts/function"
  20. import { subscribe } from "wonka"
  21. import clone from "lodash/clone"
  22. import { getAuthIDToken } from "~/helpers/fb/auth"
  23. const BACKEND_GQL_URL =
  24. process.env.CONTEXT === "production"
  25. ? "https://api.hoppscotch.io/graphql"
  26. : "https://api.hoppscotch.io/graphql"
  27. const client = createClient({
  28. url: BACKEND_GQL_URL,
  29. fetchOptions: () => {
  30. const token = getAuthIDToken()
  31. return {
  32. headers: {
  33. authorization: token ? `Bearer ${token}` : "",
  34. },
  35. }
  36. },
  37. exchanges: [devtoolsExchange, ...defaultExchanges],
  38. })
  39. /**
  40. * A wrapper type for defining errors possible in a GQL operation
  41. */
  42. export type GQLError<T extends string> =
  43. | {
  44. type: "network_error"
  45. error: Error
  46. }
  47. | {
  48. type: "gql_error"
  49. error: T
  50. }
  51. const DEFAULT_QUERY_OPTIONS = {
  52. noPolling: false,
  53. pause: undefined as Ref<boolean> | undefined,
  54. }
  55. type GQL_QUERY_OPTIONS = typeof DEFAULT_QUERY_OPTIONS
  56. type UseQueryLoading = {
  57. loading: true
  58. }
  59. type UseQueryLoaded<
  60. QueryFailType extends string = "",
  61. QueryReturnType = any
  62. > = {
  63. loading: false
  64. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  65. }
  66. type UseQueryReturn<QueryFailType extends string = "", QueryReturnType = any> =
  67. | UseQueryLoading
  68. | UseQueryLoaded<QueryFailType, QueryReturnType>
  69. export function isLoadedGQLQuery<QueryFailType extends string, QueryReturnType>(
  70. x: UseQueryReturn<QueryFailType, QueryReturnType>
  71. ): x is {
  72. loading: false
  73. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  74. } {
  75. return !x.loading
  76. }
  77. export function useGQLQuery<
  78. QueryReturnType = any,
  79. QueryFailType extends string = "",
  80. QueryVariables extends object = {}
  81. >(
  82. query: string | DocumentNode | TypedDocumentNode<any, QueryVariables>,
  83. variables?: QueryVariables,
  84. options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS
  85. ):
  86. | { loading: false; data: E.Either<GQLError<QueryFailType>, QueryReturnType> }
  87. | { loading: true } {
  88. type DataType = E.Either<GQLError<QueryFailType>, QueryReturnType>
  89. const finalOptions = Object.assign(clone(DEFAULT_QUERY_OPTIONS), options)
  90. const data = ref<DataType>()
  91. let subscription: { unsubscribe(): void } | null = null
  92. onMounted(() => {
  93. const gqlQuery = client.query<any, QueryVariables>(query, variables)
  94. const processResult = (result: OperationResult<any, QueryVariables>) =>
  95. pipe(
  96. // The target
  97. result.data as QueryReturnType | undefined,
  98. // Define what happens if data does not exist (it is an error)
  99. E.fromNullable(
  100. pipe(
  101. // Take the network error value
  102. result.error?.networkError,
  103. // If it null, set the left to the generic error name
  104. E.fromNullable(result.error?.name),
  105. E.match(
  106. // The left case (network error was null)
  107. (gqlErr) =>
  108. <GQLError<QueryFailType>>{
  109. type: "gql_error",
  110. error: gqlErr as QueryFailType,
  111. },
  112. // The right case (it was a GraphQL Error)
  113. (networkErr) =>
  114. <GQLError<QueryFailType>>{
  115. type: "network_error",
  116. error: networkErr,
  117. }
  118. )
  119. )
  120. )
  121. )
  122. if (finalOptions.noPolling) {
  123. gqlQuery.toPromise().then((result) => {
  124. data.value = processResult(result)
  125. })
  126. } else {
  127. subscription = pipe(
  128. gqlQuery,
  129. subscribe((result) => {
  130. data.value = processResult(result)
  131. })
  132. )
  133. }
  134. })
  135. onBeforeUnmount(() => {
  136. subscription?.unsubscribe()
  137. })
  138. return reactive({
  139. loading: computed(() => !data.value),
  140. data: data!,
  141. }) as
  142. | {
  143. loading: false
  144. data: DataType
  145. }
  146. | { loading: true }
  147. }
  148. export const runMutation = <
  149. MutationReturnType = any,
  150. MutationFailType extends string = "",
  151. MutationVariables extends {} = {}
  152. >(
  153. mutation: string | DocumentNode | TypedDocumentNode<any, MutationVariables>,
  154. variables?: MutationVariables
  155. ): TE.TaskEither<GQLError<MutationFailType>, NonNullable<MutationReturnType>> =>
  156. pipe(
  157. TE.tryCatch(
  158. () => client.mutation<MutationReturnType>(mutation, variables).toPromise(),
  159. () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
  160. ),
  161. TE.chainEitherK((result) =>
  162. pipe(
  163. result.data as MutationReturnType, // If we have the result, then okay
  164. E.fromNullable(
  165. // Result is null
  166. pipe(
  167. result.error?.networkError, // Check for network error
  168. E.fromNullable(result.error?.name), // If it is null, then it is a GQL error
  169. E.match(
  170. // The left case (network error was null)
  171. (gqlErr) =>
  172. <GQLError<MutationFailType>>{
  173. type: "gql_error",
  174. error: gqlErr as MutationFailType,
  175. },
  176. // The right case (it was a GraphQL Error)
  177. (networkErr) =>
  178. <GQLError<MutationFailType>>{
  179. type: "network_error",
  180. error: networkErr,
  181. }
  182. )
  183. )
  184. )
  185. )
  186. )
  187. )