GQLClient.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {
  2. computed,
  3. ref,
  4. onMounted,
  5. onBeforeUnmount,
  6. reactive,
  7. Ref,
  8. } from "@nuxtjs/composition-api"
  9. import {
  10. createClient,
  11. TypedDocumentNode,
  12. OperationResult,
  13. dedupExchange,
  14. OperationContext,
  15. fetchExchange,
  16. makeOperation,
  17. } from "@urql/core"
  18. import { authExchange } from "@urql/exchange-auth"
  19. import { offlineExchange } from "@urql/exchange-graphcache"
  20. import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
  21. import { devtoolsExchange } from "@urql/devtools"
  22. import * as E from "fp-ts/Either"
  23. import * as TE from "fp-ts/TaskEither"
  24. import { pipe, constVoid } from "fp-ts/function"
  25. import { subscribe } from "wonka"
  26. import clone from "lodash/clone"
  27. import { keyDefs } from "./caching/keys"
  28. import { optimisticDefs } from "./caching/optimistic"
  29. import { updatesDef } from "./caching/updates"
  30. import { resolversDef } from "./caching/resolvers"
  31. import schema from "./backend-schema.json"
  32. import {
  33. getAuthIDToken,
  34. probableUser$,
  35. waitProbableLoginToConfirm,
  36. } from "~/helpers/fb/auth"
  37. const BACKEND_GQL_URL =
  38. process.env.CONTEXT === "production"
  39. ? "https://api.hoppscotch.io/graphql"
  40. : "https://api.hoppscotch.io/graphql"
  41. const storage = makeDefaultStorage({
  42. idbName: "hoppcache-v1",
  43. maxAge: 7,
  44. })
  45. const client = createClient({
  46. url: BACKEND_GQL_URL,
  47. exchanges: [
  48. devtoolsExchange,
  49. dedupExchange,
  50. offlineExchange({
  51. schema: schema as any,
  52. keys: keyDefs,
  53. optimistic: optimisticDefs,
  54. updates: updatesDef,
  55. resolvers: resolversDef,
  56. storage,
  57. }),
  58. authExchange({
  59. addAuthToOperation({ authState, operation }) {
  60. if (!authState || !authState.authToken) {
  61. return operation
  62. }
  63. const fetchOptions =
  64. typeof operation.context.fetchOptions === "function"
  65. ? operation.context.fetchOptions()
  66. : operation.context.fetchOptions || {}
  67. return makeOperation(operation.kind, operation, {
  68. ...operation.context,
  69. fetchOptions: {
  70. ...fetchOptions,
  71. headers: {
  72. ...fetchOptions.headers,
  73. Authorization: `Bearer ${authState.authToken}`,
  74. },
  75. },
  76. })
  77. },
  78. willAuthError({ authState }) {
  79. return !authState || !authState.authToken
  80. },
  81. getAuth: async () => {
  82. if (!probableUser$.value) return { authToken: null }
  83. await waitProbableLoginToConfirm()
  84. return {
  85. authToken: getAuthIDToken(),
  86. }
  87. },
  88. }),
  89. fetchExchange,
  90. ],
  91. })
  92. /**
  93. * A wrapper type for defining errors possible in a GQL operation
  94. */
  95. export type GQLError<T extends string> =
  96. | {
  97. type: "network_error"
  98. error: Error
  99. }
  100. | {
  101. type: "gql_error"
  102. error: T
  103. }
  104. const DEFAULT_QUERY_OPTIONS = {
  105. noPolling: false,
  106. pause: undefined as Ref<boolean> | undefined,
  107. }
  108. type GQL_QUERY_OPTIONS = typeof DEFAULT_QUERY_OPTIONS
  109. type UseQueryLoading = {
  110. loading: true
  111. }
  112. type UseQueryLoaded<
  113. QueryFailType extends string = "",
  114. QueryReturnType = any
  115. > = {
  116. loading: false
  117. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  118. }
  119. type UseQueryReturn<QueryFailType extends string = "", QueryReturnType = any> =
  120. | UseQueryLoading
  121. | UseQueryLoaded<QueryFailType, QueryReturnType>
  122. export function isLoadedGQLQuery<QueryFailType extends string, QueryReturnType>(
  123. x: UseQueryReturn<QueryFailType, QueryReturnType>
  124. ): x is {
  125. loading: false
  126. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  127. } {
  128. return !x.loading
  129. }
  130. export function useGQLQuery<
  131. QueryReturnType = any,
  132. QueryVariables extends object = {},
  133. QueryFailType extends string = ""
  134. >(
  135. query: TypedDocumentNode<QueryReturnType, QueryVariables>,
  136. variables?: QueryVariables,
  137. options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS
  138. ):
  139. | { loading: false; data: E.Either<GQLError<QueryFailType>, QueryReturnType> }
  140. | { loading: true } {
  141. type DataType = E.Either<GQLError<QueryFailType>, QueryReturnType>
  142. const finalOptions = Object.assign(clone(DEFAULT_QUERY_OPTIONS), options)
  143. const data = ref<DataType>()
  144. let subscription: { unsubscribe(): void } | null = null
  145. onMounted(() => {
  146. const gqlQuery = client.query<any, QueryVariables>(query, variables, {
  147. requestPolicy: "cache-and-network",
  148. })
  149. const processResult = (result: OperationResult<any, QueryVariables>) =>
  150. pipe(
  151. // The target
  152. result.data as QueryReturnType | undefined,
  153. // Define what happens if data does not exist (it is an error)
  154. E.fromNullable(
  155. pipe(
  156. // Take the network error value
  157. result.error?.networkError,
  158. // If it null, set the left to the generic error name
  159. E.fromNullable(result.error?.message),
  160. E.match(
  161. // The left case (network error was null)
  162. (gqlErr) =>
  163. <GQLError<QueryFailType>>{
  164. type: "gql_error",
  165. error: gqlErr as QueryFailType,
  166. },
  167. // The right case (it was a GraphQL Error)
  168. (networkErr) =>
  169. <GQLError<QueryFailType>>{
  170. type: "network_error",
  171. error: networkErr,
  172. }
  173. )
  174. )
  175. )
  176. )
  177. if (finalOptions.noPolling) {
  178. gqlQuery.toPromise().then((result) => {
  179. data.value = processResult(result)
  180. })
  181. } else {
  182. subscription = pipe(
  183. gqlQuery,
  184. subscribe((result) => {
  185. data.value = processResult(result)
  186. })
  187. )
  188. }
  189. })
  190. onBeforeUnmount(() => {
  191. subscription?.unsubscribe()
  192. })
  193. return reactive({
  194. loading: computed(() => !data.value),
  195. data: data!,
  196. }) as
  197. | {
  198. loading: false
  199. data: DataType
  200. }
  201. | { loading: true }
  202. }
  203. export const runMutation = <
  204. DocType,
  205. DocVariables extends object | undefined,
  206. DocErrors extends string
  207. >(
  208. mutation: TypedDocumentNode<DocType, DocVariables>,
  209. variables?: DocVariables,
  210. additionalConfig?: Partial<OperationContext>
  211. ): TE.TaskEither<GQLError<DocErrors>, DocType> =>
  212. pipe(
  213. TE.tryCatch(
  214. () =>
  215. client
  216. .mutation(mutation, variables, {
  217. requestPolicy: "cache-and-network",
  218. ...additionalConfig,
  219. })
  220. .toPromise(),
  221. () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
  222. ),
  223. TE.chainEitherK((result) =>
  224. pipe(
  225. result.data,
  226. E.fromNullable(
  227. // Result is null
  228. pipe(
  229. result.error?.networkError,
  230. E.fromNullable(result.error?.name),
  231. E.match(
  232. // The left case (network error was null)
  233. (gqlErr) =>
  234. <GQLError<DocErrors>>{
  235. type: "gql_error",
  236. error: gqlErr,
  237. },
  238. // The right case (it was a GraphQL Error)
  239. (networkErr) =>
  240. <GQLError<DocErrors>>{
  241. type: "network_error",
  242. error: networkErr,
  243. }
  244. )
  245. )
  246. )
  247. )
  248. )
  249. )