|  | 
|  | 1 | +/* eslint-disable @typescript-eslint/no-explicit-any */ | 
|  | 2 | +import { | 
|  | 3 | +    injectQuery, | 
|  | 4 | +    injectMutation, | 
|  | 5 | +    injectInfiniteQuery, | 
|  | 6 | +    QueryClient, | 
|  | 7 | +    type CreateQueryOptions, | 
|  | 8 | +    type CreateMutationOptions, | 
|  | 9 | +    type CreateInfiniteQueryOptions, | 
|  | 10 | +    type InfiniteData, | 
|  | 11 | +    CreateInfiniteQueryResult, | 
|  | 12 | +    QueryKey, | 
|  | 13 | +} from '@tanstack/angular-query-v5'; | 
|  | 14 | +import type { ModelMeta } from '@zenstackhq/runtime/cross'; | 
|  | 15 | +import { inject, InjectionToken } from '@angular/core'; | 
|  | 16 | +import { | 
|  | 17 | +    APIContext, | 
|  | 18 | +    DEFAULT_QUERY_ENDPOINT, | 
|  | 19 | +    fetcher, | 
|  | 20 | +    getQueryKey, | 
|  | 21 | +    makeUrl, | 
|  | 22 | +    marshal, | 
|  | 23 | +    setupInvalidation, | 
|  | 24 | +    setupOptimisticUpdate, | 
|  | 25 | +    type ExtraMutationOptions, | 
|  | 26 | +    type ExtraQueryOptions, | 
|  | 27 | +    type FetchFn, | 
|  | 28 | +} from '../runtime/common'; | 
|  | 29 | + | 
|  | 30 | +export { APIContext as RequestHandlerContext } from '../runtime/common'; | 
|  | 31 | + | 
|  | 32 | +export const AngularQueryContextKey = new InjectionToken<APIContext>('zenstack-angular-query-context'); | 
|  | 33 | + | 
|  | 34 | +/** | 
|  | 35 | + * Provide context for the generated TanStack Query hooks. | 
|  | 36 | + */ | 
|  | 37 | +export function provideAngularQueryContext(context: APIContext) { | 
|  | 38 | +    return { | 
|  | 39 | +        provide: AngularQueryContextKey, | 
|  | 40 | +        useValue: context, | 
|  | 41 | +    }; | 
|  | 42 | +} | 
|  | 43 | + | 
|  | 44 | +/** | 
|  | 45 | + * Hooks context. | 
|  | 46 | + */ | 
|  | 47 | +export function getHooksContext() { | 
|  | 48 | +    const context = inject(AngularQueryContextKey, { | 
|  | 49 | +        optional: true, | 
|  | 50 | +    }) || { | 
|  | 51 | +        endpoint: DEFAULT_QUERY_ENDPOINT, | 
|  | 52 | +        fetch: undefined, | 
|  | 53 | +        logging: false, | 
|  | 54 | +    }; | 
|  | 55 | + | 
|  | 56 | +    const { endpoint, ...rest } = context; | 
|  | 57 | +    return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; | 
|  | 58 | +} | 
|  | 59 | + | 
|  | 60 | +/** | 
|  | 61 | + * Creates an Angular TanStack Query query. | 
|  | 62 | + * | 
|  | 63 | + * @param model The name of the model under query. | 
|  | 64 | + * @param url The request URL. | 
|  | 65 | + * @param args The request args object, URL-encoded and appended as "?q=" parameter | 
|  | 66 | + * @param options The Angular query options object | 
|  | 67 | + * @param fetch The fetch function to use for sending the HTTP request | 
|  | 68 | + * @returns injectQuery hook | 
|  | 69 | + */ | 
|  | 70 | +export function useModelQuery<TQueryFnData, TData, TError>( | 
|  | 71 | +    model: string, | 
|  | 72 | +    url: string, | 
|  | 73 | +    args?: unknown, | 
|  | 74 | +    options?: Omit<CreateQueryOptions<TQueryFnData, TError, TData>, 'queryKey'> & ExtraQueryOptions, | 
|  | 75 | +    fetch?: FetchFn | 
|  | 76 | +) { | 
|  | 77 | +    const reqUrl = makeUrl(url, args); | 
|  | 78 | +    const queryKey = getQueryKey(model, url, args, { | 
|  | 79 | +        infinite: false, | 
|  | 80 | +        optimisticUpdate: options?.optimisticUpdate !== false, | 
|  | 81 | +    }); | 
|  | 82 | +    return { | 
|  | 83 | +        queryKey, | 
|  | 84 | +        ...injectQuery(() => ({ | 
|  | 85 | +            queryKey, | 
|  | 86 | +            queryFn: ({ signal }) => fetcher<TQueryFnData, false>(reqUrl, { signal }, fetch, false), | 
|  | 87 | +            ...options, | 
|  | 88 | +        })), | 
|  | 89 | +    }; | 
|  | 90 | +} | 
|  | 91 | + | 
|  | 92 | +/** | 
|  | 93 | + * Creates an Angular TanStack Query infinite query. | 
|  | 94 | + * | 
|  | 95 | + * @param model The name of the model under query. | 
|  | 96 | + * @param url The request URL. | 
|  | 97 | + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter | 
|  | 98 | + * @param options The Angular infinite query options object | 
|  | 99 | + * @param fetch The fetch function to use for sending the HTTP request | 
|  | 100 | + * @returns injectInfiniteQuery hook | 
|  | 101 | + */ | 
|  | 102 | +export function useInfiniteModelQuery<TQueryFnData, TData, TError>( | 
|  | 103 | +    model: string, | 
|  | 104 | +    url: string, | 
|  | 105 | +    args: unknown, | 
|  | 106 | +    options: Omit< | 
|  | 107 | +        CreateInfiniteQueryOptions<TQueryFnData, TError, InfiniteData<TData>>, | 
|  | 108 | +        'queryKey' | 'initialPageParam' | 
|  | 109 | +    >, | 
|  | 110 | +    fetch?: FetchFn | 
|  | 111 | +): CreateInfiniteQueryResult<InfiniteData<TData>, TError> & { queryKey: QueryKey } { | 
|  | 112 | +    const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); | 
|  | 113 | +    return { | 
|  | 114 | +        queryKey, | 
|  | 115 | +        ...injectInfiniteQuery(() => ({ | 
|  | 116 | +            queryKey, | 
|  | 117 | +            queryFn: ({ pageParam, signal }) => { | 
|  | 118 | +                return fetcher<TQueryFnData, false>(makeUrl(url, pageParam ?? args), { signal }, fetch, false); | 
|  | 119 | +            }, | 
|  | 120 | +            initialPageParam: args, | 
|  | 121 | +            ...options, | 
|  | 122 | +        })), | 
|  | 123 | +    }; | 
|  | 124 | +} | 
|  | 125 | + | 
|  | 126 | +/** | 
|  | 127 | + * Creates an Angular TanStack Query mutation. | 
|  | 128 | + * | 
|  | 129 | + * @param model The name of the model under mutation. | 
|  | 130 | + * @param method The HTTP method. | 
|  | 131 | + * @param url The request URL. | 
|  | 132 | + * @param modelMeta The model metadata. | 
|  | 133 | + * @param options The Angular mutation options. | 
|  | 134 | + * @param fetch The fetch function to use for sending the HTTP request | 
|  | 135 | + * @param checkReadBack Whether to check for read back errors and return undefined if found. | 
|  | 136 | + * @returns injectMutation hook | 
|  | 137 | + */ | 
|  | 138 | +export function useModelMutation< | 
|  | 139 | +    TArgs, | 
|  | 140 | +    TError, | 
|  | 141 | +    R = any, | 
|  | 142 | +    C extends boolean = boolean, | 
|  | 143 | +    Result = C extends true ? R | undefined : R | 
|  | 144 | +>( | 
|  | 145 | +    model: string, | 
|  | 146 | +    method: 'POST' | 'PUT' | 'DELETE', | 
|  | 147 | +    url: string, | 
|  | 148 | +    modelMeta: ModelMeta, | 
|  | 149 | +    options?: Omit<CreateMutationOptions<Result, TError, TArgs>, 'mutationFn'> & ExtraMutationOptions, | 
|  | 150 | +    fetch?: FetchFn, | 
|  | 151 | +    checkReadBack?: C | 
|  | 152 | +) { | 
|  | 153 | +    const queryClient = inject(QueryClient); | 
|  | 154 | +    const mutationFn = (data: unknown) => { | 
|  | 155 | +        const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; | 
|  | 156 | +        const fetchInit: RequestInit = { | 
|  | 157 | +            method, | 
|  | 158 | +            ...(method !== 'DELETE' && { | 
|  | 159 | +                headers: { | 
|  | 160 | +                    'content-type': 'application/json', | 
|  | 161 | +                }, | 
|  | 162 | +                body: marshal(data), | 
|  | 163 | +            }), | 
|  | 164 | +        }; | 
|  | 165 | +        return fetcher<R, C>(reqUrl, fetchInit, fetch, checkReadBack) as Promise<Result>; | 
|  | 166 | +    }; | 
|  | 167 | + | 
|  | 168 | +    const finalOptions = { ...options, mutationFn }; | 
|  | 169 | +    const operation = url.split('/').pop(); | 
|  | 170 | +    const invalidateQueries = options?.invalidateQueries !== false; | 
|  | 171 | +    const optimisticUpdate = !!options?.optimisticUpdate; | 
|  | 172 | + | 
|  | 173 | +    if (operation) { | 
|  | 174 | +        const { logging } = getHooksContext(); | 
|  | 175 | +        if (invalidateQueries) { | 
|  | 176 | +            setupInvalidation( | 
|  | 177 | +                model, | 
|  | 178 | +                operation, | 
|  | 179 | +                modelMeta, | 
|  | 180 | +                finalOptions, | 
|  | 181 | +                (predicate) => queryClient.invalidateQueries({ predicate }), | 
|  | 182 | +                logging | 
|  | 183 | +            ); | 
|  | 184 | +        } | 
|  | 185 | + | 
|  | 186 | +        if (optimisticUpdate) { | 
|  | 187 | +            setupOptimisticUpdate( | 
|  | 188 | +                model, | 
|  | 189 | +                operation, | 
|  | 190 | +                modelMeta, | 
|  | 191 | +                finalOptions, | 
|  | 192 | +                queryClient.getQueryCache().getAll(), | 
|  | 193 | +                (queryKey, data) => { | 
|  | 194 | +                    // update query cache | 
|  | 195 | +                    queryClient.setQueryData<unknown>(queryKey, data); | 
|  | 196 | +                    // cancel on-flight queries to avoid redundant cache updates, | 
|  | 197 | +                    // the settlement of the current mutation will trigger a new revalidation | 
|  | 198 | +                    queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); | 
|  | 199 | +                }, | 
|  | 200 | +                invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, | 
|  | 201 | +                logging | 
|  | 202 | +            ); | 
|  | 203 | +        } | 
|  | 204 | +    } | 
|  | 205 | + | 
|  | 206 | +    return injectMutation(() => finalOptions); | 
|  | 207 | +} | 
0 commit comments