mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
Co-authored-by: Dominik Klein <dk@zammad.com> Co-authored-by: Benjamin Scharf <bs@zammad.com>
311 lines
9.1 KiB
TypeScript
311 lines
9.1 KiB
TypeScript
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
import { getOperationName } from '@apollo/client/utilities'
|
|
import { useApolloClient } from '@vue/apollo-composable'
|
|
import { watch, nextTick, computed } from 'vue'
|
|
|
|
import { useOnEmitter } from '#shared/composables/useOnEmitter.ts'
|
|
import { BaseHandler } from '#shared/server/apollo/handler/BaseHandler.ts'
|
|
import type {
|
|
OperationQueryOptionsReturn,
|
|
OperationQueryResult,
|
|
QueryHandlerOptions,
|
|
WatchResultCallback,
|
|
} from '#shared/types/server/apollo/handler.ts'
|
|
import type { ReactiveFunction } from '#shared/types/utils.ts'
|
|
|
|
import type {
|
|
ApolloError,
|
|
ApolloQueryResult,
|
|
FetchMoreOptions,
|
|
FetchMoreQueryOptions,
|
|
ObservableQuery,
|
|
OperationVariables,
|
|
QueryOptions,
|
|
SubscribeToMoreOptions,
|
|
Unmasked,
|
|
} from '@apollo/client/core'
|
|
import type { UseQueryOptions, UseQueryReturn } from '@vue/apollo-composable'
|
|
import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
|
|
|
|
export default class QueryHandler<
|
|
TResult = OperationQueryResult,
|
|
TVariables extends OperationVariables = OperationVariables,
|
|
TOptions extends QueryHandlerOptions = QueryHandlerOptions,
|
|
> extends BaseHandler<TResult, TVariables, UseQueryReturn<TResult, TVariables>, TOptions> {
|
|
private lastCancel: (() => void) | null = null
|
|
|
|
protected initialize(): void {
|
|
super.initialize()
|
|
|
|
this.setupReconnectionHandler()
|
|
}
|
|
|
|
private setupReconnectionHandler(): void {
|
|
if (!this.handlerOptions.triggerRefetchOnConnectionReconnect) return
|
|
|
|
useOnEmitter('reconnected', () => {
|
|
if (
|
|
(typeof this.handlerOptions.triggerRefetchOnConnectionReconnect === 'function' &&
|
|
this.handlerOptions.triggerRefetchOnConnectionReconnect()) ||
|
|
this.handlerOptions.triggerRefetchOnConnectionReconnect
|
|
) {
|
|
this.refetch().catch(() => {})
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Like `loading()`, but returns false when the Apollo cache already holds
|
|
* data for this exact query + variables. Use this instead of `loading()`
|
|
* in places where showing a spinner on the initial render would cause a
|
|
* visible flicker (e.g. page-level skeleton states with `cache-and-network`).
|
|
*
|
|
* For explicit refetch / fetchMore operations, use `loading()` as usual.
|
|
*/
|
|
public loadingWithoutCachedResult(): ComputedRef<boolean> {
|
|
return computed(() => {
|
|
if (!this.operationResult.loading.value) return false
|
|
|
|
// vue-apollo may briefly yield undefined result even when the cache is
|
|
// complete — keep showing the loader until the result ref is populated.
|
|
return this.operationResult.result.value === undefined
|
|
})
|
|
}
|
|
|
|
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 extends OperationVariables = 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> & {
|
|
updateQuery?: (
|
|
previousQueryResult: Unmasked<TResult>,
|
|
options: {
|
|
fetchMoreResult: Unmasked<TResult>
|
|
variables: TVariables
|
|
},
|
|
) => Unmasked<TResult>
|
|
},
|
|
): 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?: Partial<TVariables>,
|
|
): Promise<{ data: Maybe<TResult>; error?: unknown }> {
|
|
return new Promise((resolve, reject) => {
|
|
const refetch = this.operationResult.refetch(variables as TVariables)
|
|
|
|
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 isFirstRun(): boolean {
|
|
return this.operationResult.forceDisabled.value
|
|
}
|
|
|
|
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>) {
|
|
let watchStopHandle: WatchStopHandle | null = null
|
|
|
|
watchStopHandle = watch(
|
|
this.result(),
|
|
(result) => {
|
|
if (!watchStopHandle || !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)
|
|
}
|
|
}
|