GQLClient.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. dedupExchange,
  15. OperationContext,
  16. fetchExchange,
  17. makeOperation,
  18. } from "@urql/core"
  19. import { authExchange } from "@urql/exchange-auth"
  20. import { offlineExchange } from "@urql/exchange-graphcache"
  21. import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
  22. import { devtoolsExchange } from "@urql/devtools"
  23. import * as E from "fp-ts/Either"
  24. import * as TE from "fp-ts/TaskEither"
  25. import { pipe, constVoid } from "fp-ts/function"
  26. import { subscribe } from "wonka"
  27. import clone from "lodash/clone"
  28. import gql from "graphql-tag"
  29. import {
  30. getAuthIDToken,
  31. probableUser$,
  32. waitProbableLoginToConfirm,
  33. } from "~/helpers/fb/auth"
  34. const BACKEND_GQL_URL =
  35. process.env.CONTEXT === "production"
  36. ? "https://api.hoppscotch.io/graphql"
  37. : "https://api.hoppscotch.io/graphql"
  38. const storage = makeDefaultStorage({
  39. idbName: "hoppcache-v1",
  40. maxAge: 7,
  41. })
  42. const client = createClient({
  43. url: BACKEND_GQL_URL,
  44. exchanges: [
  45. devtoolsExchange,
  46. dedupExchange,
  47. // TODO: Extract this outttttttt
  48. offlineExchange({
  49. keys: {
  50. User: (data) => (data as any).uid,
  51. TeamMember: (data) => (data as any).membershipID,
  52. Team: (data) => data.id as any,
  53. },
  54. optimistic: {
  55. deleteTeam: () => true,
  56. leaveTeam: () => true,
  57. },
  58. updates: {
  59. Mutation: {
  60. deleteTeam: (_r, { teamID }, cache, _info) => {
  61. cache.updateQuery(
  62. {
  63. query: gql`
  64. query {
  65. myTeams {
  66. id
  67. }
  68. }
  69. `,
  70. },
  71. (data: any) => {
  72. console.log(data)
  73. data.myTeams = (data as any).myTeams.filter(
  74. (x: any) => x.id !== teamID
  75. )
  76. return data
  77. }
  78. )
  79. cache.invalidate({
  80. __typename: "Team",
  81. id: teamID as any,
  82. })
  83. },
  84. leaveTeam: (_r, { teamID }, cache, _info) => {
  85. cache.updateQuery(
  86. {
  87. query: gql`
  88. query {
  89. myTeams {
  90. id
  91. }
  92. }
  93. `,
  94. },
  95. (data: any) => {
  96. console.log(data)
  97. data.myTeams = (data as any).myTeams.filter(
  98. (x: any) => x.id !== teamID
  99. )
  100. return data
  101. }
  102. )
  103. cache.invalidate({
  104. __typename: "Team",
  105. id: teamID as any,
  106. })
  107. },
  108. createTeam: (result, _args, cache, _info) => {
  109. cache.updateQuery(
  110. {
  111. query: gql`
  112. {
  113. myTeams {
  114. id
  115. }
  116. }
  117. `,
  118. },
  119. (data: any) => {
  120. console.log(result)
  121. console.log(data)
  122. data.myTeams.push(result.createTeam)
  123. return data
  124. }
  125. )
  126. },
  127. },
  128. },
  129. storage,
  130. }),
  131. authExchange({
  132. addAuthToOperation({ authState, operation }) {
  133. if (!authState || !authState.authToken) {
  134. return operation
  135. }
  136. const fetchOptions =
  137. typeof operation.context.fetchOptions === "function"
  138. ? operation.context.fetchOptions()
  139. : operation.context.fetchOptions || {}
  140. return makeOperation(operation.kind, operation, {
  141. ...operation.context,
  142. fetchOptions: {
  143. ...fetchOptions,
  144. headers: {
  145. ...fetchOptions.headers,
  146. Authorization: `Bearer ${authState.authToken}`,
  147. },
  148. },
  149. })
  150. },
  151. willAuthError({ authState }) {
  152. return !authState || !authState.authToken
  153. },
  154. getAuth: async () => {
  155. if (!probableUser$.value) return { authToken: null }
  156. await waitProbableLoginToConfirm()
  157. return {
  158. authToken: getAuthIDToken(),
  159. }
  160. },
  161. }),
  162. fetchExchange,
  163. ],
  164. })
  165. /**
  166. * A wrapper type for defining errors possible in a GQL operation
  167. */
  168. export type GQLError<T extends string> =
  169. | {
  170. type: "network_error"
  171. error: Error
  172. }
  173. | {
  174. type: "gql_error"
  175. error: T
  176. }
  177. const DEFAULT_QUERY_OPTIONS = {
  178. noPolling: false,
  179. pause: undefined as Ref<boolean> | undefined,
  180. }
  181. type GQL_QUERY_OPTIONS = typeof DEFAULT_QUERY_OPTIONS
  182. type UseQueryLoading = {
  183. loading: true
  184. }
  185. type UseQueryLoaded<
  186. QueryFailType extends string = "",
  187. QueryReturnType = any
  188. > = {
  189. loading: false
  190. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  191. }
  192. type UseQueryReturn<QueryFailType extends string = "", QueryReturnType = any> =
  193. | UseQueryLoading
  194. | UseQueryLoaded<QueryFailType, QueryReturnType>
  195. export function isLoadedGQLQuery<QueryFailType extends string, QueryReturnType>(
  196. x: UseQueryReturn<QueryFailType, QueryReturnType>
  197. ): x is {
  198. loading: false
  199. data: E.Either<GQLError<QueryFailType>, QueryReturnType>
  200. } {
  201. return !x.loading
  202. }
  203. export function useGQLQuery<
  204. QueryReturnType = any,
  205. QueryFailType extends string = "",
  206. QueryVariables extends object = {}
  207. >(
  208. query: string | DocumentNode | TypedDocumentNode<any, QueryVariables>,
  209. variables?: QueryVariables,
  210. options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS
  211. ):
  212. | { loading: false; data: E.Either<GQLError<QueryFailType>, QueryReturnType> }
  213. | { loading: true } {
  214. type DataType = E.Either<GQLError<QueryFailType>, QueryReturnType>
  215. const finalOptions = Object.assign(clone(DEFAULT_QUERY_OPTIONS), options)
  216. const data = ref<DataType>()
  217. let subscription: { unsubscribe(): void } | null = null
  218. onMounted(() => {
  219. const gqlQuery = client.query<any, QueryVariables>(query, variables, {
  220. requestPolicy: "cache-and-network",
  221. })
  222. const processResult = (result: OperationResult<any, QueryVariables>) =>
  223. pipe(
  224. // The target
  225. result.data as QueryReturnType | undefined,
  226. // Define what happens if data does not exist (it is an error)
  227. E.fromNullable(
  228. pipe(
  229. // Take the network error value
  230. result.error?.networkError,
  231. // If it null, set the left to the generic error name
  232. E.fromNullable(result.error?.message),
  233. E.match(
  234. // The left case (network error was null)
  235. (gqlErr) =>
  236. <GQLError<QueryFailType>>{
  237. type: "gql_error",
  238. error: gqlErr as QueryFailType,
  239. },
  240. // The right case (it was a GraphQL Error)
  241. (networkErr) =>
  242. <GQLError<QueryFailType>>{
  243. type: "network_error",
  244. error: networkErr,
  245. }
  246. )
  247. )
  248. )
  249. )
  250. if (finalOptions.noPolling) {
  251. gqlQuery.toPromise().then((result) => {
  252. data.value = processResult(result)
  253. })
  254. } else {
  255. subscription = pipe(
  256. gqlQuery,
  257. subscribe((result) => {
  258. data.value = processResult(result)
  259. })
  260. )
  261. }
  262. })
  263. onBeforeUnmount(() => {
  264. subscription?.unsubscribe()
  265. })
  266. return reactive({
  267. loading: computed(() => !data.value),
  268. data: data!,
  269. }) as
  270. | {
  271. loading: false
  272. data: DataType
  273. }
  274. | { loading: true }
  275. }
  276. export const runMutation = <
  277. MutationReturnType = any,
  278. MutationFailType extends string = "",
  279. MutationVariables extends {} = {}
  280. >(
  281. mutation: string | DocumentNode | TypedDocumentNode<any, MutationVariables>,
  282. variables?: MutationVariables,
  283. additionalConfig?: Partial<OperationContext>
  284. ): TE.TaskEither<GQLError<MutationFailType>, NonNullable<MutationReturnType>> =>
  285. pipe(
  286. TE.tryCatch(
  287. () =>
  288. client
  289. .mutation<MutationReturnType>(mutation, variables, {
  290. requestPolicy: "cache-and-network",
  291. ...additionalConfig,
  292. })
  293. .toPromise(),
  294. () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
  295. ),
  296. TE.chainEitherK((result) =>
  297. pipe(
  298. result.data as MutationReturnType,
  299. E.fromNullable(
  300. // Result is null
  301. pipe(
  302. result.error?.networkError,
  303. E.fromNullable(result.error?.name),
  304. E.match(
  305. // The left case (network error was null)
  306. (gqlErr) =>
  307. <GQLError<MutationFailType>>{
  308. type: "gql_error",
  309. error: gqlErr as MutationFailType,
  310. },
  311. // The right case (it was a GraphQL Error)
  312. (networkErr) =>
  313. <GQLError<MutationFailType>>{
  314. type: "network_error",
  315. error: networkErr,
  316. }
  317. )
  318. )
  319. )
  320. )
  321. )
  322. )