Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular-query): injectQueries #8007

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { MutationState } from '@tanstack/query-core';
import type { OmitKeyof } from '@tanstack/query-core';
import type { Override } from '@tanstack/query-core';
import { Provider } from '@angular/core';
import type { QueriesObserverOptions } from '@tanstack/query-core';
import type { QueriesPlaceholderDataFunction } from '@tanstack/query-core';
import type { QueryClient } from '@tanstack/query-core';
import type { QueryFilters } from '@tanstack/query-core';
Expand Down Expand Up @@ -179,10 +180,9 @@ export interface InjectMutationStateOptions {
}

// @public (undocumented)
export function injectQueries<T extends Array<any>, TCombinedResult = QueriesResults<T>>({ queries, ...options }: {
queries: Signal<[...QueriesOptions<T>]>;
combine?: (result: QueriesResults<T>) => TCombinedResult;
}, injector?: Injector): Signal<TCombinedResult>;
export function injectQueries<T extends Array<any>, TCombinedResult = QueriesResults<T>>({ queriesFn, ...options }: {
queriesFn: (client: QueryClient) => readonly [...QueriesOptions<T>];
} & QueriesObserverOptions<TCombinedResult>, injector?: Injector): Signal<TCombinedResult>;

// @public
export function injectQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(optionsFn: (client: QueryClient) => DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, injector?: Injector): DefinedCreateQueryResult<TData, TError>;
Expand Down Expand Up @@ -219,32 +219,32 @@ export const provideQueryClient: ((value: QueryClient | (() => QueryClient)) =>
export function provideTanStackQuery(queryClient: QueryClient, ...features: Array<QueryFeatures>): EnvironmentProviders;

// Warning: (ae-forgotten-export) The symbol "MAXIMUM_DEPTH" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "QueryObserverOptionsForCreateQueries" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "GetOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "CreateQueryOptionsForInjectQueries" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "GetCreateQueryOptionsForInjectQueries" needs to be exported by the entry point index.d.ts
//
// @public
export type QueriesOptions<T extends Array<any>, TResult extends Array<any> = [], TDepth extends ReadonlyArray<number> = []> = TDepth['length'] extends MAXIMUM_DEPTH ? Array<QueryObserverOptionsForCreateQueries> : T extends [] ? [] : T extends [infer Head] ? [...TResult, GetOptions<Head>] : T extends [infer Head, ...infer Tail] ? QueriesOptions<[
...Tail
export type QueriesOptions<T extends Array<any>, TResults extends Array<any> = [], TDepth extends ReadonlyArray<number> = []> = TDepth['length'] extends MAXIMUM_DEPTH ? Array<CreateQueryOptionsForInjectQueries> : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForInjectQueries<Head>] : T extends [infer Head, ...infer Tails] ? QueriesOptions<[
...Tails
], [
...TResult,
GetOptions<Head>
...TResults,
GetCreateQueryOptionsForInjectQueries<Head>
], [
...TDepth,
1
]> : ReadonlyArray<unknown> extends T ? T : T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>> ? Array<QueryObserverOptionsForCreateQueries<TQueryFnData, TError, TData, TQueryKey>> : Array<QueryObserverOptionsForCreateQueries>;
]> : ReadonlyArray<unknown> extends T ? T : T extends Array<CreateQueryOptionsForInjectQueries<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>> ? Array<CreateQueryOptionsForInjectQueries<TQueryFnData, TError, TData, TQueryKey>> : Array<CreateQueryOptionsForInjectQueries>;

// Warning: (ae-forgotten-export) The symbol "GetResults" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "GetCreateQueryResult" needs to be exported by the entry point index.d.ts
//
// @public
export type QueriesResults<T extends Array<any>, TResult extends Array<any> = [], TDepth extends ReadonlyArray<number> = []> = TDepth['length'] extends MAXIMUM_DEPTH ? Array<QueryObserverResult> : T extends [] ? [] : T extends [infer Head] ? [...TResult, GetResults<Head>] : T extends [infer Head, ...infer Tail] ? QueriesResults<[
...Tail
export type QueriesResults<T extends Array<any>, TResults extends Array<any> = [], TDepth extends ReadonlyArray<number> = []> = TDepth['length'] extends MAXIMUM_DEPTH ? Array<QueryObserverResult> : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult<Head>] : T extends [infer Head, ...infer Tails] ? QueriesResults<[
...Tails
], [
...TResult,
GetResults<Head>
...TResults,
GetCreateQueryResult<Head>
], [
...TDepth,
1
]> : T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, any>> ? Array<QueryObserverResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError>> : Array<QueryObserverResult>;
]> : T extends Array<CreateQueryOptionsForInjectQueries<infer TQueryFnData, infer TError, infer TData, any>> ? Array<QueryObserverResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError>> : Array<QueryObserverResult>;

// @public (undocumented)
export const QUERY_CLIENT: InjectionToken<QueryClient>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { describe } from 'vitest'
import { TestBed, fakeAsync, flush } from '@angular/core/testing'
import { Component, Injector, input, signal } from '@angular/core'
import { QueryCache, QueryClient, injectQueries, provideAngularQuery } from '..'
import { setSignalInputs, simpleFetcher } from './test-utils'

describe('injectQueries', () => {
let queryCache: QueryCache
let queryClient: QueryClient

beforeEach(() => {
queryCache = new QueryCache()
queryClient = new QueryClient({ queryCache })
TestBed.configureTestingModule({
providers: [provideAngularQuery(queryClient)],
})
})

test('should return the correct results states', fakeAsync(() => {
const ids = [1, 2, 3]
const query = TestBed.runInInjectionContext(() => {
return injectQueries({
queriesFn: () =>
ids.map((id) => ({
queryKey: ['post', id],
queryFn: () =>
new Promise((resolve) => {
setTimeout(() => {
return resolve(id)
}, 0)
}),
})),
})
})
expect(query()[0]?.isPending).toBe(true)
expect(query()[0]?.data).toBe(undefined)
expect(query()[1]?.data).toBe(undefined)
expect(query()[2]?.data).toBe(undefined)
flush()
expect(query()[0]?.isPending).toBe(false)
expect(query()[0]?.data).toBe(1)
expect(query()[1]?.data).toBe(2)
expect(query()[2]?.data).toBe(3)
}))

test('should return the correct combined results states', fakeAsync(() => {
const ids = [1, 2, 3]
const query = TestBed.runInInjectionContext(() => {
return injectQueries({
queriesFn: () =>
ids.map((id) => ({
queryKey: ['post2', id],
queryFn: () =>
new Promise((resolve) => {
setTimeout(() => {
return resolve(id)
}, 0)
}),
})),
combine: (results) => {
return {
data: results.map((result) => result.data),
pending: results.some((result) => result.isPending),
}
},
})
})
expect(query().data).toStrictEqual([undefined, undefined, undefined])
expect(query().pending).toBe(true)
flush()
expect(query().data).toStrictEqual([1, 2, 3])
}))

test('should update query on options contained signal change', fakeAsync(() => {
const key = signal(['key1', 'key2'])
const spy = vi.fn(simpleFetcher)

const query = TestBed.runInInjectionContext(() => {
return injectQueries({
queriesFn: () => [
{
queryKey: key(),
queryFn: spy,
},
],
})
})
flush()
expect(spy).toHaveBeenCalledTimes(1)

expect(query()[0].status).toBe('success')

key.set(['key3'])
TestBed.flushEffects()

expect(spy).toHaveBeenCalledTimes(2)
// should call queryFn with context containing the new queryKey
expect(spy).toBeCalledWith({
meta: undefined,
queryKey: ['key3'],
signal: expect.anything(),
})
flush()
}))

test('should only run query once enabled signal is set to true', fakeAsync(() => {
const spy = vi.fn(simpleFetcher)
const enabled = signal(false)

const query = TestBed.runInInjectionContext(() => {
return injectQueries({
queriesFn: () => [
{
queryKey: ['key4'],
queryFn: spy,
enabled: enabled(),
},
],
})
})

expect(spy).not.toHaveBeenCalled()
expect(query()[0].status).toBe('pending')

enabled.set(true)
TestBed.flushEffects()
flush()
expect(spy).toHaveBeenCalledTimes(1)
expect(query()[0].status).toBe('success')
}))

test('should render with required signal inputs', fakeAsync(() => {
@Component({
selector: 'app-fake',
template: `{{ query()[0].data }}`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()

query = injectQueries({
queriesFn: () => [
{
queryKey: ['fake', this.name()],
queryFn: () => Promise.resolve(this.name()),
},
],
})
}

const fixture = TestBed.createComponent(FakeComponent)
setSignalInputs(fixture.componentInstance, {
name: 'signal-input-required-test',
})

flush()
fixture.detectChanges()

expect(fixture.debugElement.nativeElement.textContent).toEqual(
'signal-input-required-test',
)
}))

describe('injection context', () => {
test('throws NG0203 with descriptive error outside injection context', () => {
expect(() => {
injectQueries({
queriesFn: () => [
{
queryKey: ['injectionContextError'],
queryFn: simpleFetcher,
},
],
})
}).toThrowError(/NG0203(.*?)injectQueries/)
})

test('can be used outside injection context when passing an injector', () => {
const query = injectQueries(
{
queriesFn: () => [
{
queryKey: ['injectionContextError'],
queryFn: simpleFetcher,
},
],
},
TestBed.inject(Injector),
)

expect(query()[0].status).toBe('pending')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ describe('injectQuery', () => {
expect(query.status()).toBe('error')
}))

test('should render with required signal inputs', fakeAsync(async () => {
test('should render with required signal inputs', fakeAsync(() => {
@Component({
selector: 'app-fake',
template: `{{ query.data() }}`,
Expand Down Expand Up @@ -534,7 +534,7 @@ describe('injectQuery', () => {
)
}))

test('should run optionsFn in injection context', fakeAsync(async () => {
test('should run optionsFn in injection context', fakeAsync(() => {
@Injectable()
class FakeService {
getData(name: string) {
Expand Down Expand Up @@ -576,7 +576,7 @@ describe('injectQuery', () => {
expect(fixture.componentInstance.query.data()).toEqual('test name 2')
}))

test('should run optionsFn in injection context and allow passing injector to queryFn', fakeAsync(async () => {
test('should run optionsFn in injection context and allow passing injector to queryFn', fakeAsync(() => {
@Injectable()
class FakeService {
getData(name: string) {
Expand Down
8 changes: 2 additions & 6 deletions packages/angular-query-experimental/src/create-base-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import { QueryClient, notifyManager } from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { shouldThrowError } from './util'
import { lazyInit } from './util/lazy-init/lazy-init'
import type {
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
import type { QueryKey, QueryObserver } from '@tanstack/query-core'
import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types'

/**
Expand Down Expand Up @@ -89,7 +85,7 @@ export function createBaseQuery<

// observer.trackResult is not used as this optimization is not needed for Angular
const unsubscribe = observer.subscribe(
notifyManager.batchCalls((state: QueryObserverResult<TData, TError>) => {
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (
state.isError &&
Expand Down
37 changes: 14 additions & 23 deletions packages/angular-query-experimental/src/inject-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import {
QueryClient,
notifyManager,
} from '@tanstack/query-core'
import { assertInjector } from './util/assert-injector/assert-injector'
import { signalProxy } from './signal-proxy'
import { noop, shouldThrowError } from './util'
import { assertInjector } from './util/assert-injector/assert-injector'

import { lazyInit } from './util/lazy-init/lazy-init'
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
import type { CreateMutateFunction, CreateMutationResult } from './types'
import type { DefaultError } from '@tanstack/query-core'
import type { CreateMutationOptions } from './mutation-options'
import type { CreateMutateFunction, CreateMutationResult } from './types'

/**
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
Expand Down Expand Up @@ -72,26 +72,17 @@ export function injectMutation<
const result = signal(observer.getCurrentResult())

const unsubscribe = observer.subscribe(
notifyManager.batchCalls(
(
state: MutationObserverResult<
TData,
TError,
TVariables,
TContext
>,
) => {
ngZone.run(() => {
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
throw state.error
}
result.set(state)
})
},
),
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
throw state.error
}
result.set(state)
})
}),
)

destroyRef.onDestroy(unsubscribe)
Expand Down
Loading