GQLClient.ts 13 KB

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