QueryHandler.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. /* eslint-disable no-use-before-define */
  3. import { getOperationName } from '@apollo/client/utilities'
  4. import { useApolloClient } from '@vue/apollo-composable'
  5. import { watch, nextTick } from 'vue'
  6. import type {
  7. OperationQueryOptionsReturn,
  8. OperationQueryResult,
  9. WatchResultCallback,
  10. } from '#shared/types/server/apollo/handler.ts'
  11. import type { ReactiveFunction } from '#shared/types/utils.ts'
  12. import BaseHandler from './BaseHandler.ts'
  13. import type {
  14. ApolloError,
  15. ApolloQueryResult,
  16. FetchMoreOptions,
  17. FetchMoreQueryOptions,
  18. ObservableQuery,
  19. OperationVariables,
  20. QueryOptions,
  21. SubscribeToMoreOptions,
  22. } from '@apollo/client/core'
  23. import type { UseQueryOptions, UseQueryReturn } from '@vue/apollo-composable'
  24. import type { Ref, WatchStopHandle } from 'vue'
  25. export default class QueryHandler<
  26. TResult = OperationQueryResult,
  27. TVariables extends OperationVariables = OperationVariables,
  28. > extends BaseHandler<
  29. TResult,
  30. TVariables,
  31. UseQueryReturn<TResult, TVariables>
  32. > {
  33. private lastCancel: (() => void) | null = null
  34. public cancel() {
  35. this.lastCancel?.()
  36. }
  37. /**
  38. * Calls the query immidiately and returns the result in `data` property.
  39. *
  40. * Will throw an error, if used with "useQuery" instead of "useLazyQuery".
  41. *
  42. * Returns cached result, if there is one. Otherwise, will
  43. * `fetch` the result from the server.
  44. *
  45. * If called multiple times, cancels the previous query.
  46. *
  47. * Respects options that were defined in `useLazyQuery`, but can be overriden.
  48. *
  49. * If an error was throws, `data` is `null`, and `error` is the thrown error.
  50. */
  51. public async query(
  52. options: Omit<QueryOptions<TVariables, TResult>, 'query'> = {},
  53. ) {
  54. const {
  55. options: defaultOptions,
  56. document: { value: node },
  57. } = this.operationResult
  58. if (import.meta.env.DEV && !node) {
  59. throw new Error(`No query document available.`)
  60. }
  61. if (import.meta.env.DEV && !('load' in this.operationResult)) {
  62. let error = `${getOperationName(
  63. node!,
  64. )} is initialized with "useQuery" instead of "useLazyQuery". `
  65. error += `If you need to get the value immediately with ".query()", use "useLazyQuery" instead to not start extra network requests. `
  66. error += `"useQuery" should be used inside components to dynamically react to changed data.`
  67. throw new Error(error)
  68. }
  69. this.cancel()
  70. const { client } = useApolloClient()
  71. const aborter =
  72. typeof AbortController !== 'undefined' ? new AbortController() : null
  73. this.lastCancel = () => aborter?.abort()
  74. const { fetchPolicy: defaultFetchPolicy, ...defaultOptionsValue } =
  75. 'value' in defaultOptions ? defaultOptions.value : defaultOptions
  76. const fetchPolicy =
  77. options.fetchPolicy ||
  78. (defaultFetchPolicy !== 'cache-and-network'
  79. ? defaultFetchPolicy
  80. : undefined)
  81. try {
  82. return await client.query<TResult, TVariables>({
  83. ...defaultOptionsValue,
  84. ...options,
  85. fetchPolicy,
  86. query: node!,
  87. context: {
  88. ...defaultOptionsValue.context,
  89. ...options.context,
  90. fetchOptions: {
  91. signal: aborter?.signal,
  92. },
  93. },
  94. })
  95. } catch (error) {
  96. // TODO: do we need to handleError here also in a genric way?
  97. return {
  98. data: null,
  99. error: error as ApolloError,
  100. }
  101. } finally {
  102. this.lastCancel = null
  103. }
  104. }
  105. public options(): OperationQueryOptionsReturn<TResult, TVariables> {
  106. return this.operationResult.options
  107. }
  108. public result(): Ref<TResult | undefined> {
  109. return this.operationResult.result
  110. }
  111. public watchQuery(): Ref<
  112. ObservableQuery<TResult, TVariables> | null | undefined
  113. > {
  114. return this.operationResult.query
  115. }
  116. public subscribeToMore<
  117. TSubscriptionVariables = TVariables,
  118. TSubscriptionData = TResult,
  119. >(
  120. options:
  121. | SubscribeToMoreOptions<
  122. TResult,
  123. TSubscriptionVariables,
  124. TSubscriptionData
  125. >
  126. | ReactiveFunction<
  127. SubscribeToMoreOptions<
  128. TResult,
  129. TSubscriptionVariables,
  130. TSubscriptionData
  131. >
  132. >,
  133. ): void {
  134. return this.operationResult.subscribeToMore(options)
  135. }
  136. public fetchMore(
  137. options: FetchMoreQueryOptions<TVariables, TResult> &
  138. FetchMoreOptions<TResult, TVariables>,
  139. ): Promise<Maybe<TResult>> {
  140. return new Promise((resolve, reject) => {
  141. const fetchMore = this.operationResult.fetchMore(options)
  142. if (!fetchMore) {
  143. resolve(null)
  144. return
  145. }
  146. fetchMore
  147. .then((result) => {
  148. resolve(result.data)
  149. })
  150. .catch(() => {
  151. reject(this.operationError().value)
  152. })
  153. })
  154. }
  155. public refetch(
  156. variables?: TVariables,
  157. ): Promise<{ data: Maybe<TResult>; error?: unknown }> {
  158. return new Promise((resolve, reject) => {
  159. const refetch = this.operationResult.refetch(variables)
  160. if (!refetch) {
  161. resolve({ data: null })
  162. return
  163. }
  164. refetch
  165. .then((result) => {
  166. resolve({ data: result.data })
  167. })
  168. .catch(() => {
  169. reject(this.operationError().value)
  170. })
  171. })
  172. }
  173. public load(
  174. variables?: TVariables,
  175. options?: UseQueryOptions<TResult, TVariables>,
  176. ): void {
  177. const operation = this.operationResult as unknown as {
  178. load?: (
  179. document?: unknown,
  180. variables?: TVariables,
  181. options?: UseQueryOptions<TResult, TVariables>,
  182. ) => false | Promise<TResult>
  183. }
  184. if (typeof operation.load !== 'function') {
  185. return
  186. }
  187. const result = operation.load(undefined, variables, options)
  188. if (result instanceof Promise) {
  189. // error is handled in BaseHandler
  190. result.catch(() => {})
  191. }
  192. }
  193. public start(): void {
  194. this.operationResult.start()
  195. }
  196. public stop(): void {
  197. this.operationResult.stop()
  198. }
  199. public abort() {
  200. this.operationResult.stop()
  201. this.operationResult.start()
  202. }
  203. public watchOnceOnResult(callback: WatchResultCallback<TResult>) {
  204. const watchStopHandle = watch(
  205. this.result(),
  206. (result) => {
  207. if (!result) {
  208. return
  209. }
  210. callback(result)
  211. watchStopHandle()
  212. },
  213. {
  214. // Needed for when the component is mounted after the first mount, in this case
  215. // result will already contain the data and the watch will otherwise not be triggered.
  216. immediate: true,
  217. },
  218. )
  219. }
  220. public watchOnResult(
  221. callback: WatchResultCallback<TResult>,
  222. ): WatchStopHandle {
  223. return watch(
  224. this.result(),
  225. (result) => {
  226. if (!result) {
  227. return
  228. }
  229. callback(result)
  230. },
  231. {
  232. // Needed for when the component is mounted after the first mount, in this case
  233. // result will already contain the data and the watch will otherwise not be triggered.
  234. immediate: true,
  235. },
  236. )
  237. }
  238. public onResult(
  239. callback: (result: ApolloQueryResult<TResult | undefined>) => void,
  240. ignoreFirstResult?: boolean,
  241. ): void {
  242. if (ignoreFirstResult) {
  243. this.watchOnceOnResult(() => {
  244. nextTick(() => {
  245. this.operationResult.onResult(callback)
  246. })
  247. })
  248. return
  249. }
  250. this.operationResult.onResult(callback)
  251. }
  252. }