From 39c1c851acb656626feb345ccc4f64f6c3a059b0 Mon Sep 17 00:00:00 2001 From: Dji75 Date: Mon, 30 Jun 2025 18:50:04 +0200 Subject: [PATCH] feat(query): add provideQueryConfig to provide custom queries objects --- README.md | 45 +++++ query/src/index.ts | 1 + query/src/lib/infinite-query.ts | 69 +++++-- query/src/lib/is-fetching.ts | 61 ++++-- query/src/lib/is-mutating.ts | 60 ++++-- query/src/lib/mutation.ts | 71 +++++-- query/src/lib/provide-query-config.ts | 49 +++++ query/src/lib/query.ts | 85 ++++++-- query/src/tests/provide-query-config.spec.ts | 201 +++++++++++++++++++ query/src/tests/query.spec.ts | 131 +++++++++++- 10 files changed, 707 insertions(+), 66 deletions(-) create mode 100644 query/src/lib/provide-query-config.ts create mode 100644 query/src/tests/provide-query-config.spec.ts diff --git a/README.md b/README.md index 7205b16..5c5161e 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ export class TodosService { For methods that require a `queryFn` parameter like `ensureQueryData`, `fetchQuery`, `prefetchQuery`, `fetchInfiniteQuery` and `prefetchInfiniteQuery` it's possible to use both Promises and Observables. See an example [here](https://github.com/ngneat/query/blob/main/src/app/prefetch-page/resolve.ts#L9). + #### Component Usage - Observable To get an observable use the `result$` property: @@ -626,6 +627,50 @@ class TodoComponent { } ``` +## Custom Queries objects + +All injected queries objects got from the following inject functions could be overwritten by using `provideQueryConfig` function: +- `injectQuery()` +- `injectMutation()` +- `injectIsMutating()` +- `injectIsFetching()` +- `injectInfiniteQuery()` + +All `provideQueryConfig` parameter's properties are optional, and you can use raw object or object factory. + +```ts +export function provideQueryConfig( + config: { + query?: QueryObject | (() => QueryObject); + mutation?: MutationObject | (() => MutationObject); + isMutating?: IsMutatingObject | (() => IsMutatingObject); + isFetching?: IsFetchingObject | (() => IsFetchingObject); + infiniteQuery?: InfiniteQueryObject | (() => InfiniteQueryObject); + }, +): Provider +``` + +### Mock Example +```ts +import { provideQueryConfig } from '@ngneat/query'; + +const queryMock = { + use: () => ({ + result$: of({ ... }), + result: computed(() => ({ ... })), + }) +}; + +provideQueryConfig({ + query: queryMock, // or query: () => queryMock if you need a factory +}); + +const query = injectQuery(); +query({ ... }).result() // you can call query from your custom query mock +``` + + + ## Devtools Install the `@ngneat/query-devtools` package. Lazy load and use it only in `development` environment: diff --git a/query/src/index.ts b/query/src/index.ts index 48287a6..b236e0d 100644 --- a/query/src/index.ts +++ b/query/src/index.ts @@ -20,3 +20,4 @@ export { createSuccessObserverResult, toPromise, } from './lib/utils'; +export { provideQueryConfig } from './lib/provide-query-config'; diff --git a/query/src/lib/infinite-query.ts b/query/src/lib/infinite-query.ts index 015b698..39181ff 100644 --- a/query/src/lib/infinite-query.ts +++ b/query/src/lib/infinite-query.ts @@ -1,12 +1,11 @@ import { assertInInjectionContext, inject, - Injectable, + InjectionToken, Injector, runInInjectionContext, } from '@angular/core'; import { injectQueryClient } from './query-client'; - import { DefaultError, InfiniteData, @@ -24,6 +23,17 @@ import { } from './base-query'; import { Result } from './types'; +/** @internal */ +export const InfiniteQueryToken = new InjectionToken( + 'InfiniteQuery', + { + providedIn: 'root', + factory() { + return new InfiniteQuery(); + }, + }, +); + interface _CreateInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -61,8 +71,28 @@ export type CreateInfiniteQueryOptions< queryFn: QueryFunctionWithObservable; }; -@Injectable({ providedIn: 'root' }) -class InfiniteQuery { +export interface InfiniteQueryObject { + use: < + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ) => Result>; +} + +/** @internal + * only exported for @test + */ +class InfiniteQuery implements InfiniteQueryObject { #instance = injectQueryClient(); #injector = inject(Injector); @@ -90,18 +120,33 @@ class InfiniteQuery { } } +function infiniteQueryUseFnFromToken() { + const infiniteQuery = inject(InfiniteQueryToken); + return infiniteQuery.use.bind(infiniteQuery); +} + +/** + * + * Optionally pass an injector that will be used than the current one. + * Can be useful if you want to use it in ngOnInit hook for example. + * + * @example + * + * injector = inject(Injector); + * + * ngOnInit() { + * const infiniteQuery = injectInfiniteQuery({ injector: this.injector }); + * } + * + */ export function injectInfiniteQuery(options?: { injector?: Injector }) { if (options?.injector) { - return runInInjectionContext(options.injector, () => { - const query = inject(InfiniteQuery); - - return query.use.bind(query); - }); + return runInInjectionContext(options.injector, () => + infiniteQueryUseFnFromToken(), + ); } assertInInjectionContext(injectInfiniteQuery); - const query = inject(InfiniteQuery); - - return query.use.bind(query); + return infiniteQueryUseFnFromToken(); } diff --git a/query/src/lib/is-fetching.ts b/query/src/lib/is-fetching.ts index 597ea08..36c7dda 100644 --- a/query/src/lib/is-fetching.ts +++ b/query/src/lib/is-fetching.ts @@ -3,14 +3,33 @@ import { injectQueryClient } from './query-client'; import { assertInInjectionContext, inject, - Injectable, InjectionToken, + Injector, + runInInjectionContext, + Signal, } from '@angular/core'; import { distinctUntilChanged, Observable } from 'rxjs'; import { toSignal } from '@angular/core/rxjs-interop'; -@Injectable({ providedIn: 'root' }) -export class IsFetching { +/** @internal */ +export const IsFetchingToken = new InjectionToken('IsFetching', { + providedIn: 'root', + factory() { + return new IsFetching(); + }, +}); + +export interface IsFetchingObject { + use: (filters?: QueryFilters) => { + result$: Observable; + toSignal: () => Signal; + }; +} + +/** @internal + * only exported for @test + */ +export class IsFetching implements IsFetchingObject { #queryClient = injectQueryClient(); use(filters?: QueryFilters) { @@ -32,15 +51,33 @@ export class IsFetching { } } -const UseIsFetching = new InjectionToken('UseIsFetching', { - providedIn: 'root', - factory() { - const isFetching = new IsFetching(); - return isFetching.use.bind(isFetching); - }, -}); +function isFetchingUseFnFromToken() { + const isFetching = inject(IsFetchingToken); + return isFetching.use.bind(isFetching); +} + +/** + * + * Optionally pass an injector that will be used than the current one. + * Can be useful if you want to use it in ngOnInit hook for example. + * + * @example + * + * injector = inject(Injector); + * + * ngOnInit() { + * const isFetching = injectIsFetching({ injector: this.injector }); + * } + * + */ +export function injectIsFetching(options?: { injector?: Injector }) { + if (options?.injector) { + return runInInjectionContext(options.injector, () => + isFetchingUseFnFromToken(), + ); + } -export function injectIsFetching() { assertInInjectionContext(injectIsFetching); - return inject(UseIsFetching); + + return isFetchingUseFnFromToken(); } diff --git a/query/src/lib/is-mutating.ts b/query/src/lib/is-mutating.ts index b986fee..58d68e4 100644 --- a/query/src/lib/is-mutating.ts +++ b/query/src/lib/is-mutating.ts @@ -3,14 +3,33 @@ import { injectQueryClient } from './query-client'; import { assertInInjectionContext, inject, - Injectable, InjectionToken, + Injector, + runInInjectionContext, + Signal, } from '@angular/core'; import { distinctUntilChanged, Observable } from 'rxjs'; import { toSignal } from '@angular/core/rxjs-interop'; -@Injectable({ providedIn: 'root' }) -export class IsMutating { +/** @internal */ +export const IsMutatingToken = new InjectionToken('IsMutating', { + providedIn: 'root', + factory() { + return new IsMutating(); + }, +}); + +export interface IsMutatingObject { + use: (filters?: MutationFilters) => { + result$: Observable; + toSignal: () => Signal; + }; +} + +/** @internal + * only exported for @test + */ +export class IsMutating implements IsMutatingObject { #queryClient = injectQueryClient(); use(filters?: MutationFilters) { @@ -34,16 +53,33 @@ export class IsMutating { } } -const UseIsMutating = new InjectionToken('UseIsFetching', { - providedIn: 'root', - factory() { - const isMutating = new IsMutating(); - return isMutating.use.bind(isMutating); - }, -}); +function isMutatingUseFnFromToken() { + const isMutating = inject(IsMutatingToken); + return isMutating.use.bind(isMutating); +} + +/** + * + * Optionally pass an injector that will be used than the current one. + * Can be useful if you want to use it in ngOnInit hook for example. + * + * @example + * + * injector = inject(Injector); + * + * ngOnInit() { + * const isMutating = injectIsMutating({ injector: this.injector }); + * } + * + */ +export function injectIsMutating(options?: { injector?: Injector }) { + if (options?.injector) { + return runInInjectionContext(options.injector, () => + isMutatingUseFnFromToken(), + ); + } -export function injectIsMutating() { assertInInjectionContext(injectIsMutating); - return inject(UseIsMutating); + return isMutatingUseFnFromToken(); } diff --git a/query/src/lib/mutation.ts b/query/src/lib/mutation.ts index f625595..abf235e 100644 --- a/query/src/lib/mutation.ts +++ b/query/src/lib/mutation.ts @@ -1,4 +1,11 @@ -import { inject, Injectable, InjectionToken, Signal } from '@angular/core'; +import { + assertInInjectionContext, + inject, + InjectionToken, + Injector, + runInInjectionContext, + Signal, +} from '@angular/core'; import { injectQueryClient } from './query-client'; import { DefaultError, @@ -12,6 +19,14 @@ import { isObservable, Observable, shareReplay } from 'rxjs'; import { toSignal } from '@angular/core/rxjs-interop'; import { shouldThrowError, toPromise } from './utils'; +/** @internal */ +export const MutationToken = new InjectionToken('Mutation', { + providedIn: 'root', + factory() { + return new Mutation(); + }, +}); + export type CreateMutationOptions< TData = unknown, TError = DefaultError, @@ -51,8 +66,21 @@ export type MutationResult< result: Signal>; }; -@Injectable({ providedIn: 'root' }) -class Mutation { +export interface MutationObject { + use: < + TData = unknown, + TError = DefaultError, + TVariables = unknown, + TContext = unknown, + >( + options: CreateMutationOptions, + ) => MutationResult; +} + +/** @internal + * only exported for @test + */ +class Mutation implements MutationObject { #instance = injectQueryClient(); use< @@ -141,14 +169,33 @@ class Mutation { } } -const UseMutation = new InjectionToken('UseMutation', { - providedIn: 'root', - factory() { - const mutation = new Mutation(); - return mutation.use.bind(mutation); - }, -}); +function mutationUseFnFromToken() { + const mutation = inject(MutationToken); + return mutation.use.bind(mutation); +} + +/** + * + * Optionally pass an injector that will be used than the current one. + * Can be useful if you want to use it in ngOnInit hook for example. + * + * @example + * + * injector = inject(Injector); + * + * ngOnInit() { + * const mutation = injectMutation({ injector: this.injector }); + * } + * + */ +export function injectMutation(options?: { injector?: Injector }) { + if (options?.injector) { + return runInInjectionContext(options.injector, () => + mutationUseFnFromToken(), + ); + } + + assertInInjectionContext(injectMutation); -export function injectMutation() { - return inject(UseMutation); + return mutationUseFnFromToken(); } diff --git a/query/src/lib/provide-query-config.ts b/query/src/lib/provide-query-config.ts new file mode 100644 index 0000000..c2551e8 --- /dev/null +++ b/query/src/lib/provide-query-config.ts @@ -0,0 +1,49 @@ +import { InjectionToken, Provider } from '@angular/core'; + +import { QueryObject, QueryToken } from './query'; +import { MutationObject, MutationToken } from './mutation'; +import { IsMutatingObject, IsMutatingToken } from './is-mutating'; +import { IsFetchingObject, IsFetchingToken } from './is-fetching'; +import { InfiniteQueryObject, InfiniteQueryToken } from './infinite-query'; + +function providerBuilder( + token: InjectionToken, + valueOrFactory: Kind | (() => Kind), +): Provider { + return { + provide: token, + useFactory: + typeof valueOrFactory === 'function' + ? valueOrFactory + : () => valueOrFactory, + }; +} + +/** @public */ +export function provideQueryConfig( + config: { + query?: QueryObject | (() => QueryObject); + mutation?: MutationObject | (() => MutationObject); + isMutating?: IsMutatingObject | (() => IsMutatingObject); + isFetching?: IsFetchingObject | (() => IsFetchingObject); + infiniteQuery?: InfiniteQueryObject | (() => InfiniteQueryObject); + }, +): Provider { + const providers: Provider = []; + if (config.query) { + providers.push(providerBuilder(QueryToken, config.query)); + } + if (config.mutation) { + providers.push(providerBuilder(MutationToken, config.mutation)); + } + if (config.isMutating) { + providers.push(providerBuilder(IsMutatingToken, config.isMutating)); + } + if (config.isFetching) { + providers.push(providerBuilder(IsFetchingToken, config.isFetching)); + } + if (config.infiniteQuery) { + providers.push(providerBuilder(InfiniteQueryToken, config.infiniteQuery)); + } + return providers; +} diff --git a/query/src/lib/query.ts b/query/src/lib/query.ts index a035f1c..e4251d9 100644 --- a/query/src/lib/query.ts +++ b/query/src/lib/query.ts @@ -1,12 +1,10 @@ import { assertInInjectionContext, inject, - Injectable, + InjectionToken, Injector, runInInjectionContext, } from '@angular/core'; -import { injectQueryClient } from './query-client'; - import { DefaultError, DefinedQueryObserverResult, @@ -14,17 +12,73 @@ import { QueryObserver, QueryObserverResult, } from '@tanstack/query-core'; + import { createBaseQuery, CreateBaseQueryOptions } from './base-query'; -import { Result } from './types'; +import { injectQueryClient } from './query-client'; import { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './query-options'; +import { Result } from './types'; + +/** @internal */ +export const QueryToken = new InjectionToken('Query', { + providedIn: 'root', + factory() { + return new Query(); + }, +}); -@Injectable({ providedIn: 'root' }) -class Query { - #instance = injectQueryClient(); - #injector = inject(Injector); +export interface QueryObject { + use: + | (< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: UndefinedInitialDataOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) => Result>) + | (< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: DefinedInitialDataOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) => Result>) + | (< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: CreateBaseQueryOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + >, + ) => any); +} + +/** @internal + * only exported for @test + */ +export class Query implements QueryObject { + readonly #instance = injectQueryClient(); + readonly #injector = inject(Injector); use< TQueryFnData = unknown, @@ -72,6 +126,11 @@ class Query { } } +function queryUseFnFromToken() { + const query = inject(QueryToken); + return query.use.bind(query); +} + /** * * Optionally pass an injector that will be used than the current one. @@ -88,16 +147,10 @@ class Query { */ export function injectQuery(options?: { injector?: Injector }) { if (options?.injector) { - return runInInjectionContext(options.injector, () => { - const query = inject(Query); - - return query.use.bind(query); - }); + return runInInjectionContext(options.injector, () => queryUseFnFromToken()); } assertInInjectionContext(injectQuery); - const query = inject(Query); - - return query.use.bind(query); + return queryUseFnFromToken(); } diff --git a/query/src/tests/provide-query-config.spec.ts b/query/src/tests/provide-query-config.spec.ts new file mode 100644 index 0000000..e808176 --- /dev/null +++ b/query/src/tests/provide-query-config.spec.ts @@ -0,0 +1,201 @@ +import { TestBed } from '@angular/core/testing'; +import { provideQueryConfig } from '../lib/provide-query-config'; +import { injectQuery, QueryObject } from '../lib/query'; +import { injectMutation, MutationObject } from '../lib/mutation'; +import { of } from 'rxjs'; +import { injectIsMutating, IsMutatingObject } from '../lib/is-mutating'; +import { injectIsFetching, IsFetchingObject } from '../lib/is-fetching'; +import { + InfiniteQueryObject, + injectInfiniteQuery, +} from '../lib/infinite-query'; + +describe('Provide Query Config', () => { + describe('Custom Query', () => { + const queryMock: QueryObject = { + use: jest.fn(() => {}), + }; + + it('should use custom query', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ query: queryMock })], + }); + const query = TestBed.runInInjectionContext(() => injectQuery()); + const queryOptions = { + queryKey: ['test1', 'test3'], + queryFn: () => 'test2', + }; + query(queryOptions); + + expect(queryMock.use).lastCalledWith(queryOptions); + }); + + it('should use custom query provided from function', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ query: () => queryMock })], + }); + const query = TestBed.runInInjectionContext(() => injectQuery()); + const queryOptions = { + queryKey: ['test1', 'test2'], + queryFn: () => 'test3', + }; + query(queryOptions); + + expect(queryMock.use).lastCalledWith(queryOptions); + }); + }); + + describe('Custom Mutation', () => { + const mutationMock: MutationObject = { + use: jest.fn(() => {}) as any, + }; + + it('should use custom mutation', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ mutation: mutationMock })], + }); + const mutation = TestBed.runInInjectionContext(() => injectMutation()); + const mutationOptions = { + onSuccess: () => ['test1', 'test3'], + mutationFn: () => of('test2'), + }; + mutation(mutationOptions); + + expect(mutationMock.use).lastCalledWith(mutationOptions); + }); + + it('should use custom mutation provided from function', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ mutation: () => mutationMock })], + }); + const mutation = TestBed.runInInjectionContext(() => injectMutation()); + const mutationOptions = { + onSuccess: () => ['test1', 'test2'], + mutationFn: () => of('test3'), + }; + mutation(mutationOptions); + + expect(mutationMock.use).lastCalledWith(mutationOptions); + }); + }); + + describe('Custom IsMutating', () => { + const isMutatingMock: IsMutatingObject = { + use: jest.fn(() => {}) as any, + }; + + it('should use custom isMutating', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ isMutating: isMutatingMock })], + }); + const isMutating = TestBed.runInInjectionContext(() => + injectIsMutating(), + ); + const isMutatingOptions = { + exact: true, + predicate: () => false, + }; + isMutating(isMutatingOptions); + + expect(isMutatingMock.use).lastCalledWith(isMutatingOptions); + }); + + it('should use custom isMutating provided from function', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ isMutating: () => isMutatingMock })], + }); + const isMutating = TestBed.runInInjectionContext(() => + injectIsMutating(), + ); + const isMutatingOptions = { + exact: false, + predicate: () => true, + }; + isMutating(isMutatingOptions); + + expect(isMutatingMock.use).lastCalledWith(isMutatingOptions); + }); + }); + + describe('Custom IsFetching', () => { + const isFetchingMock: IsFetchingObject = { + use: jest.fn(() => {}) as any, + }; + + it('should use custom isFetching', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ isFetching: isFetchingMock })], + }); + const isFetching = TestBed.runInInjectionContext(() => + injectIsFetching(), + ); + const isFetchingOptions = { + exact: true, + predicate: () => false, + }; + isFetching(isFetchingOptions); + + expect(isFetchingMock.use).lastCalledWith(isFetchingOptions); + }); + + it('should use custom isFetching provided from function', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ isFetching: () => isFetchingMock })], + }); + const isFetching = TestBed.runInInjectionContext(() => + injectIsFetching(), + ); + const isFetchingOptions = { + exact: false, + predicate: () => true, + }; + isFetching(isFetchingOptions); + + expect(isFetchingMock.use).lastCalledWith(isFetchingOptions); + }); + }); + + describe('Custom InfiniteQuery', () => { + const infiniteQueryMock: InfiniteQueryObject = { + use: jest.fn(() => {}) as any, + }; + + it('should use custom isFetching', () => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ infiniteQuery: infiniteQueryMock })], + }); + const infiniteQuery = TestBed.runInInjectionContext(() => + injectInfiniteQuery(), + ); + const infiniteQueryOptions = { + queryKey: ['test1', 'test2'], + queryFn: () => 'test3', + getNextPageParam: () => 'test4', + initialPageParam: 'test5', + }; + infiniteQuery(infiniteQueryOptions); + + expect(infiniteQueryMock.use).lastCalledWith(infiniteQueryOptions); + }); + + it('should use custom infiniteQuery provided from function', () => { + TestBed.configureTestingModule({ + providers: [ + provideQueryConfig({ infiniteQuery: () => infiniteQueryMock }), + ], + }); + const infiniteQuery = TestBed.runInInjectionContext(() => + injectInfiniteQuery(), + ); + const infiniteQueryOptions = { + queryKey: ['test1', 'test3'], + queryFn: () => 'test2', + getNextPageParam: () => 'test5', + initialPageParam: 'test4', + }; + infiniteQuery(infiniteQueryOptions); + + expect(infiniteQueryMock.use).lastCalledWith(infiniteQueryOptions); + }); + }); +}); diff --git a/query/src/tests/query.spec.ts b/query/src/tests/query.spec.ts index 5a935cd..df887d8 100644 --- a/query/src/tests/query.spec.ts +++ b/query/src/tests/query.spec.ts @@ -1,7 +1,17 @@ -import { Injector, effect, runInInjectionContext } from '@angular/core'; -import { TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; +import { effect, Injector, runInInjectionContext, Signal } from '@angular/core'; +import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { expectTypeOf } from 'expect-type'; +import { + DefaultError, + QueryKey, + QueryObserverResult, +} from '@tanstack/query-core'; +import { BehaviorSubject, Observable } from 'rxjs'; import { Todo, TodosService } from './test-helper'; +import { injectQuery } from '../lib/query'; +import { UndefinedInitialDataOptions } from '../lib/query-options'; +import { Result } from '../lib/types'; +import { provideQueryConfig } from '../lib/provide-query-config'; describe('query', () => { let service: TodosService; @@ -56,3 +66,120 @@ describe('query', () => { expect(spy).toHaveBeenCalledWith('success'); })); }); + +describe('Custom Query', () => { + const mockResultInitial = { + status: 'success', + error: null, + isPending: false, + isSuccess: true, + isError: false, + isFetching: false, + isStale: true, + }; + const result$ = new BehaviorSubject< + typeof mockResultInitial & { options?: unknown } + >(mockResultInitial); + const result = () => result$.getValue(); + const queryMock = { + use< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: UndefinedInitialDataOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ): Result> { + result$.next({ ...mockResultInitial, options }); + return { + result: result as Signal>, + result$: result$.asObservable() as Observable< + QueryObserverResult + >, + updateOptions: < + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey, + >( + options2: UndefinedInitialDataOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) => { + result$.next({ ...result$.getValue(), options: options2 }); + }, + }; + }, + }; + + it('should use custom query', (done) => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ query: queryMock })], + }); + const query = TestBed.runInInjectionContext(() => injectQuery()); + + const queryOptions = { + queryKey: ['test1', 'test3'], + queryFn: () => 'test2', + }; + const queryResult = query(queryOptions); + expect(queryResult.result()).toEqual({ + ...mockResultInitial, + options: queryOptions, + }); + queryResult.result$.subscribe((result: QueryObserverResult) => { + expect(result).toMatchObject(mockResultInitial); + expect( + 'options' in result && + 'queryKey' in (result.options as object) && + (result.options as typeof queryOptions).queryKey, + ).toEqual(['test1', 'test3']); + expect( + 'options' in result && + 'queryFn' in (result.options as object) && + (result.options as typeof queryOptions).queryFn(), + ).toEqual('test2'); + done(); + }); + }); + + it('should use custom query client provided from function', (done) => { + TestBed.configureTestingModule({ + providers: [provideQueryConfig({ query: () => queryMock })], + }); + const query = TestBed.runInInjectionContext(() => injectQuery()); + + const queryOptions = { + queryKey: ['test1', 'test2'], + queryFn: () => 'test3', + }; + const queryResult = query(queryOptions); + + expect(queryResult.result()).toEqual({ + ...mockResultInitial, + options: queryOptions, + }); + queryResult.result$.subscribe((result) => { + expect(result).toMatchObject(mockResultInitial); + expect( + 'options' in result && + 'queryKey' in (result.options as object) && + (result.options as typeof queryOptions).queryKey, + ).toEqual(['test1', 'test2']); + expect( + 'options' in result && + 'queryFn' in (result.options as object) && + (result.options as typeof queryOptions).queryFn(), + ).toEqual('test3'); + done(); + }); + }); +});