Skip to content

Commit fabfdf8

Browse files
authored
feat(query): add provideQueryConfig to provide custom queries objects (#213)
1 parent 9392ccb commit fabfdf8

File tree

10 files changed

+707
-66
lines changed

10 files changed

+707
-66
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export class TodosService {
109109
For methods that require a `queryFn` parameter like
110110
`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).
111111

112+
112113
#### Component Usage - Observable
113114

114115
To get an observable use the `result$` property:
@@ -626,6 +627,50 @@ class TodoComponent {
626627
}
627628
```
628629

630+
## Custom Queries objects
631+
632+
All injected queries objects got from the following inject functions could be overwritten by using `provideQueryConfig` function:
633+
- `injectQuery()`
634+
- `injectMutation()`
635+
- `injectIsMutating()`
636+
- `injectIsFetching()`
637+
- `injectInfiniteQuery()`
638+
639+
All `provideQueryConfig` parameter's properties are optional, and you can use raw object or object factory.
640+
641+
```ts
642+
export function provideQueryConfig(
643+
config: {
644+
query?: QueryObject | (() => QueryObject);
645+
mutation?: MutationObject | (() => MutationObject);
646+
isMutating?: IsMutatingObject | (() => IsMutatingObject);
647+
isFetching?: IsFetchingObject | (() => IsFetchingObject);
648+
infiniteQuery?: InfiniteQueryObject | (() => InfiniteQueryObject);
649+
},
650+
): Provider
651+
```
652+
653+
### Mock Example
654+
```ts
655+
import { provideQueryConfig } from '@ngneat/query';
656+
657+
const queryMock = {
658+
use: () => ({
659+
result$: of({ ... }),
660+
result: computed(() => ({ ... })),
661+
})
662+
};
663+
664+
provideQueryConfig({
665+
query: queryMock, // or query: () => queryMock if you need a factory
666+
});
667+
668+
const query = injectQuery();
669+
query({ ... }).result() // you can call query from your custom query mock
670+
```
671+
672+
673+
629674
## Devtools
630675

631676
Install the `@ngneat/query-devtools` package. Lazy load and use it only in `development` environment:

query/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export {
2020
createSuccessObserverResult,
2121
toPromise,
2222
} from './lib/utils';
23+
export { provideQueryConfig } from './lib/provide-query-config';

query/src/lib/infinite-query.ts

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import {
22
assertInInjectionContext,
33
inject,
4-
Injectable,
4+
InjectionToken,
55
Injector,
66
runInInjectionContext,
77
} from '@angular/core';
88
import { injectQueryClient } from './query-client';
9-
109
import {
1110
DefaultError,
1211
InfiniteData,
@@ -24,6 +23,17 @@ import {
2423
} from './base-query';
2524
import { Result } from './types';
2625

26+
/** @internal */
27+
export const InfiniteQueryToken = new InjectionToken<InfiniteQuery>(
28+
'InfiniteQuery',
29+
{
30+
providedIn: 'root',
31+
factory() {
32+
return new InfiniteQuery();
33+
},
34+
},
35+
);
36+
2737
interface _CreateInfiniteQueryOptions<
2838
TQueryFnData = unknown,
2939
TError = DefaultError,
@@ -61,8 +71,28 @@ export type CreateInfiniteQueryOptions<
6171
queryFn: QueryFunctionWithObservable<TQueryFnData, TQueryKey, TPageParam>;
6272
};
6373

64-
@Injectable({ providedIn: 'root' })
65-
class InfiniteQuery {
74+
export interface InfiniteQueryObject {
75+
use: <
76+
TQueryFnData,
77+
TError = DefaultError,
78+
TData = InfiniteData<TQueryFnData>,
79+
TQueryKey extends QueryKey = QueryKey,
80+
TPageParam = unknown,
81+
>(
82+
options: CreateInfiniteQueryOptions<
83+
TQueryFnData,
84+
TError,
85+
TData,
86+
TQueryKey,
87+
TPageParam
88+
>,
89+
) => Result<InfiniteQueryObserverResult<TData, TError>>;
90+
}
91+
92+
/** @internal
93+
* only exported for @test
94+
*/
95+
class InfiniteQuery implements InfiniteQueryObject {
6696
#instance = injectQueryClient();
6797
#injector = inject(Injector);
6898

@@ -90,18 +120,33 @@ class InfiniteQuery {
90120
}
91121
}
92122

123+
function infiniteQueryUseFnFromToken() {
124+
const infiniteQuery = inject(InfiniteQueryToken);
125+
return infiniteQuery.use.bind(infiniteQuery);
126+
}
127+
128+
/**
129+
*
130+
* Optionally pass an injector that will be used than the current one.
131+
* Can be useful if you want to use it in ngOnInit hook for example.
132+
*
133+
* @example
134+
*
135+
* injector = inject(Injector);
136+
*
137+
* ngOnInit() {
138+
* const infiniteQuery = injectInfiniteQuery({ injector: this.injector });
139+
* }
140+
*
141+
*/
93142
export function injectInfiniteQuery(options?: { injector?: Injector }) {
94143
if (options?.injector) {
95-
return runInInjectionContext(options.injector, () => {
96-
const query = inject(InfiniteQuery);
97-
98-
return query.use.bind(query);
99-
});
144+
return runInInjectionContext(options.injector, () =>
145+
infiniteQueryUseFnFromToken(),
146+
);
100147
}
101148

102149
assertInInjectionContext(injectInfiniteQuery);
103150

104-
const query = inject(InfiniteQuery);
105-
106-
return query.use.bind(query);
151+
return infiniteQueryUseFnFromToken();
107152
}

query/src/lib/is-fetching.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,33 @@ import { injectQueryClient } from './query-client';
33
import {
44
assertInInjectionContext,
55
inject,
6-
Injectable,
76
InjectionToken,
7+
Injector,
8+
runInInjectionContext,
9+
Signal,
810
} from '@angular/core';
911
import { distinctUntilChanged, Observable } from 'rxjs';
1012
import { toSignal } from '@angular/core/rxjs-interop';
1113

12-
@Injectable({ providedIn: 'root' })
13-
export class IsFetching {
14+
/** @internal */
15+
export const IsFetchingToken = new InjectionToken<IsFetching>('IsFetching', {
16+
providedIn: 'root',
17+
factory() {
18+
return new IsFetching();
19+
},
20+
});
21+
22+
export interface IsFetchingObject {
23+
use: (filters?: QueryFilters) => {
24+
result$: Observable<number>;
25+
toSignal: () => Signal<number | undefined>;
26+
};
27+
}
28+
29+
/** @internal
30+
* only exported for @test
31+
*/
32+
export class IsFetching implements IsFetchingObject {
1433
#queryClient = injectQueryClient();
1534

1635
use(filters?: QueryFilters) {
@@ -32,15 +51,33 @@ export class IsFetching {
3251
}
3352
}
3453

35-
const UseIsFetching = new InjectionToken<IsFetching['use']>('UseIsFetching', {
36-
providedIn: 'root',
37-
factory() {
38-
const isFetching = new IsFetching();
39-
return isFetching.use.bind(isFetching);
40-
},
41-
});
54+
function isFetchingUseFnFromToken() {
55+
const isFetching = inject(IsFetchingToken);
56+
return isFetching.use.bind(isFetching);
57+
}
58+
59+
/**
60+
*
61+
* Optionally pass an injector that will be used than the current one.
62+
* Can be useful if you want to use it in ngOnInit hook for example.
63+
*
64+
* @example
65+
*
66+
* injector = inject(Injector);
67+
*
68+
* ngOnInit() {
69+
* const isFetching = injectIsFetching({ injector: this.injector });
70+
* }
71+
*
72+
*/
73+
export function injectIsFetching(options?: { injector?: Injector }) {
74+
if (options?.injector) {
75+
return runInInjectionContext(options.injector, () =>
76+
isFetchingUseFnFromToken(),
77+
);
78+
}
4279

43-
export function injectIsFetching() {
4480
assertInInjectionContext(injectIsFetching);
45-
return inject(UseIsFetching);
81+
82+
return isFetchingUseFnFromToken();
4683
}

query/src/lib/is-mutating.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,33 @@ import { injectQueryClient } from './query-client';
33
import {
44
assertInInjectionContext,
55
inject,
6-
Injectable,
76
InjectionToken,
7+
Injector,
8+
runInInjectionContext,
9+
Signal,
810
} from '@angular/core';
911
import { distinctUntilChanged, Observable } from 'rxjs';
1012
import { toSignal } from '@angular/core/rxjs-interop';
1113

12-
@Injectable({ providedIn: 'root' })
13-
export class IsMutating {
14+
/** @internal */
15+
export const IsMutatingToken = new InjectionToken<IsMutating>('IsMutating', {
16+
providedIn: 'root',
17+
factory() {
18+
return new IsMutating();
19+
},
20+
});
21+
22+
export interface IsMutatingObject {
23+
use: (filters?: MutationFilters) => {
24+
result$: Observable<number>;
25+
toSignal: () => Signal<number | undefined>;
26+
};
27+
}
28+
29+
/** @internal
30+
* only exported for @test
31+
*/
32+
export class IsMutating implements IsMutatingObject {
1433
#queryClient = injectQueryClient();
1534

1635
use(filters?: MutationFilters) {
@@ -34,16 +53,33 @@ export class IsMutating {
3453
}
3554
}
3655

37-
const UseIsMutating = new InjectionToken<IsMutating['use']>('UseIsFetching', {
38-
providedIn: 'root',
39-
factory() {
40-
const isMutating = new IsMutating();
41-
return isMutating.use.bind(isMutating);
42-
},
43-
});
56+
function isMutatingUseFnFromToken() {
57+
const isMutating = inject(IsMutatingToken);
58+
return isMutating.use.bind(isMutating);
59+
}
60+
61+
/**
62+
*
63+
* Optionally pass an injector that will be used than the current one.
64+
* Can be useful if you want to use it in ngOnInit hook for example.
65+
*
66+
* @example
67+
*
68+
* injector = inject(Injector);
69+
*
70+
* ngOnInit() {
71+
* const isMutating = injectIsMutating({ injector: this.injector });
72+
* }
73+
*
74+
*/
75+
export function injectIsMutating(options?: { injector?: Injector }) {
76+
if (options?.injector) {
77+
return runInInjectionContext(options.injector, () =>
78+
isMutatingUseFnFromToken(),
79+
);
80+
}
4481

45-
export function injectIsMutating() {
4682
assertInInjectionContext(injectIsMutating);
4783

48-
return inject(UseIsMutating);
84+
return isMutatingUseFnFromToken();
4985
}

0 commit comments

Comments
 (0)