123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- /* eslint-disable no-use-before-define */
- import { getOperationName } from '@apollo/client/utilities'
- import { useApolloClient } from '@vue/apollo-composable'
- import { watch, nextTick } from 'vue'
- import type {
- OperationQueryOptionsReturn,
- OperationQueryResult,
- WatchResultCallback,
- } from '#shared/types/server/apollo/handler.ts'
- import type { ReactiveFunction } from '#shared/types/utils.ts'
- import BaseHandler from './BaseHandler.ts'
- import type {
- ApolloError,
- ApolloQueryResult,
- FetchMoreOptions,
- FetchMoreQueryOptions,
- ObservableQuery,
- OperationVariables,
- QueryOptions,
- SubscribeToMoreOptions,
- } from '@apollo/client/core'
- import type { UseQueryOptions, UseQueryReturn } from '@vue/apollo-composable'
- import type { Ref, WatchStopHandle } from 'vue'
- export default class QueryHandler<
- TResult = OperationQueryResult,
- TVariables extends OperationVariables = OperationVariables,
- > extends BaseHandler<
- TResult,
- TVariables,
- UseQueryReturn<TResult, TVariables>
- > {
- private lastCancel: (() => void) | null = null
- public cancel() {
- this.lastCancel?.()
- }
- /**
- * Calls the query immidiately and returns the result in `data` property.
- *
- * Will throw an error, if used with "useQuery" instead of "useLazyQuery".
- *
- * Returns cached result, if there is one. Otherwise, will
- * `fetch` the result from the server.
- *
- * If called multiple times, cancels the previous query.
- *
- * Respects options that were defined in `useLazyQuery`, but can be overriden.
- *
- * If an error was throws, `data` is `null`, and `error` is the thrown error.
- */
- public async query(
- options: Omit<QueryOptions<TVariables, TResult>, 'query'> = {},
- ) {
- const {
- options: defaultOptions,
- document: { value: node },
- } = this.operationResult
- if (import.meta.env.DEV && !node) {
- throw new Error(`No query document available.`)
- }
- if (import.meta.env.DEV && !('load' in this.operationResult)) {
- let error = `${getOperationName(
- node!,
- )} is initialized with "useQuery" instead of "useLazyQuery". `
- error += `If you need to get the value immediately with ".query()", use "useLazyQuery" instead to not start extra network requests. `
- error += `"useQuery" should be used inside components to dynamically react to changed data.`
- throw new Error(error)
- }
- this.cancel()
- const { client } = useApolloClient()
- const aborter =
- typeof AbortController !== 'undefined' ? new AbortController() : null
- this.lastCancel = () => aborter?.abort()
- const { fetchPolicy: defaultFetchPolicy, ...defaultOptionsValue } =
- 'value' in defaultOptions ? defaultOptions.value : defaultOptions
- const fetchPolicy =
- options.fetchPolicy ||
- (defaultFetchPolicy !== 'cache-and-network'
- ? defaultFetchPolicy
- : undefined)
- try {
- return await client.query<TResult, TVariables>({
- ...defaultOptionsValue,
- ...options,
- fetchPolicy,
- query: node!,
- context: {
- ...defaultOptionsValue.context,
- ...options.context,
- fetchOptions: {
- signal: aborter?.signal,
- },
- },
- })
- } catch (error) {
- // TODO: do we need to handleError here also in a genric way?
- return {
- data: null,
- error: error as ApolloError,
- }
- } finally {
- this.lastCancel = null
- }
- }
- public options(): OperationQueryOptionsReturn<TResult, TVariables> {
- return this.operationResult.options
- }
- public result(): Ref<TResult | undefined> {
- return this.operationResult.result
- }
- public watchQuery(): Ref<
- ObservableQuery<TResult, TVariables> | null | undefined
- > {
- return this.operationResult.query
- }
- public subscribeToMore<
- TSubscriptionVariables = TVariables,
- TSubscriptionData = TResult,
- >(
- options:
- | SubscribeToMoreOptions<
- TResult,
- TSubscriptionVariables,
- TSubscriptionData
- >
- | ReactiveFunction<
- SubscribeToMoreOptions<
- TResult,
- TSubscriptionVariables,
- TSubscriptionData
- >
- >,
- ): void {
- return this.operationResult.subscribeToMore(options)
- }
- public fetchMore(
- options: FetchMoreQueryOptions<TVariables, TResult> &
- FetchMoreOptions<TResult, TVariables>,
- ): Promise<Maybe<TResult>> {
- return new Promise((resolve, reject) => {
- const fetchMore = this.operationResult.fetchMore(options)
- if (!fetchMore) {
- resolve(null)
- return
- }
- fetchMore
- .then((result) => {
- resolve(result.data)
- })
- .catch(() => {
- reject(this.operationError().value)
- })
- })
- }
- public refetch(
- variables?: TVariables,
- ): Promise<{ data: Maybe<TResult>; error?: unknown }> {
- return new Promise((resolve, reject) => {
- const refetch = this.operationResult.refetch(variables)
- if (!refetch) {
- resolve({ data: null })
- return
- }
- refetch
- .then((result) => {
- resolve({ data: result.data })
- })
- .catch(() => {
- reject(this.operationError().value)
- })
- })
- }
- public load(
- variables?: TVariables,
- options?: UseQueryOptions<TResult, TVariables>,
- ): void {
- const operation = this.operationResult as unknown as {
- load?: (
- document?: unknown,
- variables?: TVariables,
- options?: UseQueryOptions<TResult, TVariables>,
- ) => false | Promise<TResult>
- }
- if (typeof operation.load !== 'function') {
- return
- }
- const result = operation.load(undefined, variables, options)
- if (result instanceof Promise) {
- // error is handled in BaseHandler
- result.catch(() => {})
- }
- }
- public start(): void {
- this.operationResult.start()
- }
- public stop(): void {
- this.operationResult.stop()
- }
- public abort() {
- this.operationResult.stop()
- this.operationResult.start()
- }
- public watchOnceOnResult(callback: WatchResultCallback<TResult>) {
- const watchStopHandle = watch(
- this.result(),
- (result) => {
- if (!result) {
- return
- }
- callback(result)
- watchStopHandle()
- },
- {
- // Needed for when the component is mounted after the first mount, in this case
- // result will already contain the data and the watch will otherwise not be triggered.
- immediate: true,
- },
- )
- }
- public watchOnResult(
- callback: WatchResultCallback<TResult>,
- ): WatchStopHandle {
- return watch(
- this.result(),
- (result) => {
- if (!result) {
- return
- }
- callback(result)
- },
- {
- // Needed for when the component is mounted after the first mount, in this case
- // result will already contain the data and the watch will otherwise not be triggered.
- immediate: true,
- },
- )
- }
- public onResult(
- callback: (result: ApolloQueryResult<TResult | undefined>) => void,
- ignoreFirstResult?: boolean,
- ): void {
- if (ignoreFirstResult) {
- this.watchOnceOnResult(() => {
- nextTick(() => {
- this.operationResult.onResult(callback)
- })
- })
- return
- }
- this.operationResult.onResult(callback)
- }
- }
|