GQLClient.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. // TODO: fix cache
  2. import {
  3. ref,
  4. reactive,
  5. Ref,
  6. unref,
  7. watchEffect,
  8. watchSyncEffect,
  9. WatchStopHandle,
  10. set,
  11. isRef,
  12. } from "@nuxtjs/composition-api"
  13. import {
  14. createClient,
  15. TypedDocumentNode,
  16. OperationResult,
  17. dedupExchange,
  18. OperationContext,
  19. fetchExchange,
  20. makeOperation,
  21. GraphQLRequest,
  22. createRequest,
  23. subscriptionExchange,
  24. } from "@urql/core"
  25. import { authExchange } from "@urql/exchange-auth"
  26. // import { offlineExchange } from "@urql/exchange-graphcache"
  27. // import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
  28. import { devtoolsExchange } from "@urql/devtools"
  29. import { SubscriptionClient } from "subscriptions-transport-ws"
  30. import * as E from "fp-ts/Either"
  31. import * as TE from "fp-ts/TaskEither"
  32. import { pipe, constVoid } from "fp-ts/function"
  33. import { Source, subscribe, pipe as wonkaPipe, onEnd } from "wonka"
  34. import { Subject } from "rxjs"
  35. // import { keyDefs } from "./caching/keys"
  36. // import { optimisticDefs } from "./caching/optimistic"
  37. // import { updatesDef } from "./caching/updates"
  38. // import { resolversDef } from "./caching/resolvers"
  39. // import schema from "./backend-schema.json"
  40. import {
  41. authIdToken$,
  42. getAuthIDToken,
  43. probableUser$,
  44. waitProbableLoginToConfirm,
  45. } from "~/helpers/fb/auth"
  46. const BACKEND_GQL_URL =
  47. process.env.context === "production"
  48. ? "https://api.hoppscotch.io/graphql"
  49. : "https://api.hoppscotch.io/graphql"
  50. // const storage = makeDefaultStorage({
  51. // idbName: "hoppcache-v1",
  52. // maxAge: 7,
  53. // })
  54. const subscriptionClient = new SubscriptionClient(
  55. process.env.context === "production"
  56. ? "wss://api.hoppscotch.io/graphql"
  57. : "wss://api.hoppscotch.io/graphql",
  58. {
  59. reconnect: true,
  60. connectionParams: () => {
  61. return {
  62. authorization: `Bearer ${authIdToken$.value}`,
  63. }
  64. },
  65. }
  66. )
  67. authIdToken$.subscribe(() => {
  68. subscriptionClient.client?.close()
  69. })
  70. const createHoppClient = () =>
  71. createClient({
  72. url: BACKEND_GQL_URL,
  73. exchanges: [
  74. devtoolsExchange,
  75. dedupExchange,
  76. // offlineExchange({
  77. // schema: schema as any,
  78. // keys: keyDefs,
  79. // optimistic: optimisticDefs,
  80. // updates: updatesDef,
  81. // resolvers: resolversDef,
  82. // storage,
  83. // }),
  84. authExchange({
  85. addAuthToOperation({ authState, operation }) {
  86. if (!authState || !authState.authToken) {
  87. return operation
  88. }
  89. const fetchOptions =
  90. typeof operation.context.fetchOptions === "function"
  91. ? operation.context.fetchOptions()
  92. : operation.context.fetchOptions || {}
  93. return makeOperation(operation.kind, operation, {
  94. ...operation.context,
  95. fetchOptions: {
  96. ...fetchOptions,
  97. headers: {
  98. ...fetchOptions.headers,
  99. Authorization: `Bearer ${authState.authToken}`,
  100. },
  101. },
  102. })
  103. },
  104. willAuthError({ authState }) {
  105. return !authState || !authState.authToken
  106. },
  107. getAuth: async () => {
  108. if (!probableUser$.value) return { authToken: null }
  109. await waitProbableLoginToConfirm()
  110. return {
  111. authToken: getAuthIDToken(),
  112. }
  113. },
  114. }),
  115. fetchExchange,
  116. subscriptionExchange({
  117. forwardSubscription: (operation) =>
  118. subscriptionClient.request(operation),
  119. }),
  120. ],
  121. })
  122. export const client = ref(createHoppClient())
  123. authIdToken$.subscribe(() => {
  124. client.value = createHoppClient()
  125. })
  126. type MaybeRef<X> = X | Ref<X>
  127. type UseQueryOptions<T = any, V = object> = {
  128. query: TypedDocumentNode<T, V>
  129. variables?: MaybeRef<V>
  130. updateSubs?: MaybeRef<GraphQLRequest<any, object>[]>
  131. defer?: boolean
  132. pollDuration?: number | undefined
  133. }
  134. type RunQueryOptions<T = any, V = object> = {
  135. query: TypedDocumentNode<T, V>
  136. variables?: V
  137. }
  138. /**
  139. * A wrapper type for defining errors possible in a GQL operation
  140. */
  141. export type GQLError<T extends string> =
  142. | {
  143. type: "network_error"
  144. error: Error
  145. }
  146. | {
  147. type: "gql_error"
  148. error: T
  149. }
  150. export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
  151. args: RunQueryOptions<DocType, DocVarType>
  152. ): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
  153. const request = createRequest<DocType, DocVarType>(args.query, args.variables)
  154. const source = client.value.executeQuery(request, {
  155. requestPolicy: "network-only",
  156. })
  157. return new Promise((resolve) => {
  158. const sub = wonkaPipe(
  159. source,
  160. subscribe((res) => {
  161. if (sub) {
  162. sub.unsubscribe()
  163. }
  164. pipe(
  165. // The target
  166. res.data as DocType | undefined,
  167. // Define what happens if data does not exist (it is an error)
  168. E.fromNullable(
  169. pipe(
  170. // Take the network error value
  171. res.error?.networkError,
  172. // If it null, set the left to the generic error name
  173. E.fromNullable(res.error?.message),
  174. E.match(
  175. // The left case (network error was null)
  176. (gqlErr) =>
  177. <GQLError<DocErrorType>>{
  178. type: "gql_error",
  179. error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
  180. },
  181. // The right case (it was a GraphQL Error)
  182. (networkErr) =>
  183. <GQLError<DocErrorType>>{
  184. type: "network_error",
  185. error: networkErr,
  186. }
  187. )
  188. )
  189. ),
  190. resolve
  191. )
  192. })
  193. )
  194. })
  195. }
  196. export const runGQLSubscription = <
  197. DocType,
  198. DocVarType,
  199. DocErrorType extends string
  200. >(
  201. args: RunQueryOptions<DocType, DocVarType>
  202. ) => {
  203. const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
  204. const source = client.value.executeSubscription(
  205. createRequest(args.query, args.variables)
  206. )
  207. wonkaPipe(
  208. source,
  209. subscribe((res) => {
  210. result$.next(
  211. pipe(
  212. // The target
  213. res.data as DocType | undefined,
  214. // Define what happens if data does not exist (it is an error)
  215. E.fromNullable(
  216. pipe(
  217. // Take the network error value
  218. res.error?.networkError,
  219. // If it null, set the left to the generic error name
  220. E.fromNullable(res.error?.message),
  221. E.match(
  222. // The left case (network error was null)
  223. (gqlErr) =>
  224. <GQLError<DocErrorType>>{
  225. type: "gql_error",
  226. error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
  227. },
  228. // The right case (it was a GraphQL Error)
  229. (networkErr) =>
  230. <GQLError<DocErrorType>>{
  231. type: "network_error",
  232. error: networkErr,
  233. }
  234. )
  235. )
  236. )
  237. )
  238. )
  239. })
  240. )
  241. return result$
  242. }
  243. export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
  244. _args: UseQueryOptions<DocType, DocVarType>
  245. ) => {
  246. const stops: WatchStopHandle[] = []
  247. const args = reactive(_args)
  248. const loading: Ref<boolean> = ref(true)
  249. const isStale: Ref<boolean> = ref(true)
  250. const data: Ref<E.Either<GQLError<DocErrorType>, DocType>> = ref() as any
  251. if (!args.updateSubs) set(args, "updateSubs", [])
  252. const isPaused: Ref<boolean> = ref(args.defer ?? false)
  253. const pollDuration: Ref<number | null> = ref(args.pollDuration ?? null)
  254. const request: Ref<GraphQLRequest<DocType, DocVarType>> = ref(
  255. createRequest<DocType, DocVarType>(
  256. args.query,
  257. unref<DocVarType>(args.variables as any) as any
  258. )
  259. ) as any
  260. const source: Ref<Source<OperationResult> | undefined> = ref()
  261. // A ref used to force re-execution of the query
  262. const updateTicker: Ref<boolean> = ref(true)
  263. // Toggles between true and false to cause the polling operation to tick
  264. const pollerTick: Ref<boolean> = ref(true)
  265. stops.push(
  266. watchEffect((onInvalidate) => {
  267. if (pollDuration.value !== null && !isPaused.value) {
  268. const handle = setInterval(() => {
  269. pollerTick.value = !pollerTick.value
  270. }, pollDuration.value)
  271. onInvalidate(() => {
  272. clearInterval(handle)
  273. })
  274. }
  275. })
  276. )
  277. stops.push(
  278. watchEffect(
  279. () => {
  280. const newRequest = createRequest<DocType, DocVarType>(
  281. args.query,
  282. unref<DocVarType>(args.variables as any) as any
  283. )
  284. if (request.value.key !== newRequest.key) {
  285. request.value = newRequest
  286. }
  287. },
  288. { flush: "pre" }
  289. )
  290. )
  291. stops.push(
  292. watchEffect(
  293. () => {
  294. // Just listen to the polling ticks
  295. // eslint-disable-next-line no-unused-expressions
  296. pollerTick.value
  297. // Just keep track of update ticking, but don't do anything
  298. // eslint-disable-next-line no-unused-expressions
  299. updateTicker.value
  300. source.value = !isPaused.value
  301. ? client.value.executeQuery<DocType, DocVarType>(request.value, {
  302. requestPolicy: "network-only",
  303. })
  304. : undefined
  305. },
  306. { flush: "pre" }
  307. )
  308. )
  309. watchSyncEffect((onInvalidate) => {
  310. if (source.value) {
  311. loading.value = true
  312. isStale.value = false
  313. const invalidateStops = args.updateSubs!.map((sub) => {
  314. return wonkaPipe(
  315. client.value.executeSubscription(sub),
  316. onEnd(() => {
  317. if (source.value) execute()
  318. }),
  319. subscribe(() => {
  320. return execute()
  321. })
  322. ).unsubscribe
  323. })
  324. invalidateStops.push(
  325. wonkaPipe(
  326. source.value,
  327. onEnd(() => {
  328. loading.value = false
  329. isStale.value = false
  330. }),
  331. subscribe((res) => {
  332. if (res.operation.key === request.value.key) {
  333. data.value = pipe(
  334. // The target
  335. res.data as DocType | undefined,
  336. // Define what happens if data does not exist (it is an error)
  337. E.fromNullable(
  338. pipe(
  339. // Take the network error value
  340. res.error?.networkError,
  341. // If it null, set the left to the generic error name
  342. E.fromNullable(res.error?.message),
  343. E.match(
  344. // The left case (network error was null)
  345. (gqlErr) =>
  346. <GQLError<DocErrorType>>{
  347. type: "gql_error",
  348. error: parseGQLErrorString(
  349. gqlErr ?? ""
  350. ) as DocErrorType,
  351. },
  352. // The right case (it was a GraphQL Error)
  353. (networkErr) =>
  354. <GQLError<DocErrorType>>{
  355. type: "network_error",
  356. error: networkErr,
  357. }
  358. )
  359. )
  360. )
  361. )
  362. loading.value = false
  363. }
  364. })
  365. ).unsubscribe
  366. )
  367. onInvalidate(() => invalidateStops.forEach((unsub) => unsub()))
  368. }
  369. })
  370. const execute = (updatedVars?: DocVarType) => {
  371. if (updatedVars) {
  372. if (isRef(args.variables)) {
  373. args.variables.value = updatedVars
  374. } else {
  375. set(args, "variables", updatedVars)
  376. }
  377. }
  378. isPaused.value = false
  379. updateTicker.value = !updateTicker.value
  380. }
  381. const pause = () => {
  382. isPaused.value = true
  383. }
  384. const unpause = () => {
  385. isPaused.value = false
  386. }
  387. const response = reactive({
  388. loading,
  389. data,
  390. pause,
  391. unpause,
  392. isStale,
  393. execute,
  394. })
  395. return response
  396. }
  397. const parseGQLErrorString = (s: string) =>
  398. s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s
  399. export const runMutation = <
  400. DocType,
  401. DocVariables extends object | undefined,
  402. DocErrors extends string
  403. >(
  404. mutation: TypedDocumentNode<DocType, DocVariables>,
  405. variables?: DocVariables,
  406. additionalConfig?: Partial<OperationContext>
  407. ): TE.TaskEither<GQLError<DocErrors>, DocType> =>
  408. pipe(
  409. TE.tryCatch(
  410. () =>
  411. client.value
  412. .mutation(mutation, variables, {
  413. requestPolicy: "cache-and-network",
  414. ...additionalConfig,
  415. })
  416. .toPromise(),
  417. () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
  418. ),
  419. TE.chainEitherK((result) =>
  420. pipe(
  421. result.data,
  422. E.fromNullable(
  423. // Result is null
  424. pipe(
  425. result.error?.networkError,
  426. E.fromNullable(result.error?.message),
  427. E.match(
  428. // The left case (network error was null)
  429. (gqlErr) =>
  430. <GQLError<DocErrors>>{
  431. type: "gql_error",
  432. error: parseGQLErrorString(gqlErr ?? ""),
  433. },
  434. // The right case (it was a network error)
  435. (networkErr) =>
  436. <GQLError<DocErrors>>{
  437. type: "network_error",
  438. error: networkErr,
  439. }
  440. )
  441. )
  442. )
  443. )
  444. )
  445. )