Skip to content

Commit 467f4b4

Browse files
authored
Merge pull request #355 from OpenAPI-Qraft/feat/get-mutation-cache
feat(react-client): add `getMutationCache()` support
2 parents bc596c5 + 039800e commit 467f4b4

File tree

19 files changed

+1065
-6
lines changed

19 files changed

+1065
-6
lines changed

.changeset/bumpy-ads-tie.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@openapi-qraft/tanstack-query-react-plugin': minor
3+
'@openapi-qraft/tanstack-query-react-types': minor
4+
'@openapi-qraft/react': minor
5+
---
6+
7+
Added `getMutationCache()` method to query client operations.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@openapi-qraft/react': minor
3+
---
4+
5+
getMutationCache() auto-sets `exact: false` for base filters
6+
7+
When calling `getMutationCache().find()` or `getMutationCache().findAll()` without providing `parameters` or
8+
`mutationKey` filters, the `exact` option is now automatically set to `false`.
9+
10+
**Why this change?**
11+
12+
TanStack Query sets `exact: true` by default for mutation filters. Without this automatic override,
13+
no mutations would match when using only predicate functions or no filters at all,
14+
since the base mutation key wouldn't match exactly against specific mutation instances.
15+
16+
**Example:**
17+
18+
```ts
19+
// Returns all mutations for the endpoint due to auto-set exact: false
20+
const mutations = qraft.entities.postEntitiesIdDocuments.getMutationCache().findAll();
21+
```
22+
23+
This ensures that predicate-based filtering and endpoint-wide mutation searches work as expected.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { OperationSchema } from '@openapi-qraft/tanstack-query-react-types';
2+
import type { CreateAPIQueryClientOptions } from '../qraftAPIClient.js';
3+
import { composeMutationFilters } from '../lib/composeMutationFilters.js';
4+
5+
export function getMutationCache(
6+
qraftOptions: CreateAPIQueryClientOptions,
7+
schema: OperationSchema
8+
) {
9+
const mutationCache = qraftOptions.queryClient.getMutationCache();
10+
11+
return wrapMutationCacheMethods(schema, mutationCache);
12+
}
13+
14+
function wrapMutationCacheMethods<
15+
T extends Record<'find' | 'findAll', (...args: any[]) => any>,
16+
>(schema: OperationSchema, original: T): T {
17+
return new Proxy(original, {
18+
get(target, prop, receiver) {
19+
if (prop === 'find' || prop === 'findAll') {
20+
return function (...args: any[]) {
21+
return Reflect.apply(target[prop], target, [
22+
composeMutationFilters(schema, args[0] as never),
23+
...args.slice(1, args.length),
24+
]);
25+
};
26+
}
27+
28+
return Reflect.get(target, prop, receiver);
29+
},
30+
});
31+
}

packages/react-client/src/callbacks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { removeQueries } from './removeQueries.js';
2323
export { refetchQueries } from './refetchQueries.js';
2424
export { isFetching } from './isFetching.js';
2525
export { isMutating } from './isMutating.js';
26+
export { getMutationCache } from './getMutationCache.js';
2627
export { useIsFetching } from './useIsFetching.js';
2728
export { fetchQuery } from './fetchQuery.js';
2829
export { prefetchQuery } from './prefetchQuery.js';

packages/react-client/src/lib/composeMutationFilters.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function composeMutationFilters<Filters extends object>(
1313
) {
1414
if (!filters) {
1515
return {
16+
exact: false,
1617
mutationKey: composeMutationKey(schema, undefined),
1718
};
1819
}
@@ -38,7 +39,8 @@ export function composeMutationFilters<Filters extends object>(
3839
}
3940

4041
return {
41-
...filters,
42+
exact: false,
4243
mutationKey: composeMutationKey(schema, undefined),
44+
...filters,
4345
};
4446
}

packages/react-client/src/tests/composeMutationFilters.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('composeMutationFilters', () => {
1414
const result = composeMutationFilters(schema, undefined);
1515

1616
expect(result).toEqual({
17+
exact: false,
1718
mutationKey: [schema, {}],
1819
});
1920
});
@@ -22,6 +23,7 @@ describe('composeMutationFilters', () => {
2223
const result = composeMutationFilters(schema, { predicate });
2324

2425
expect(result).toEqual({
26+
exact: false,
2527
mutationKey: [schema, {}],
2628
predicate,
2729
});

packages/react-client/src/tests/qraftAPIClient.test.tsx

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,255 @@ describe('Qraft uses Mutation State', () => {
18201820
});
18211821
});
18221822

1823+
describe('Qraft uses getMutationCache', () => {
1824+
const parameters = {
1825+
header: {
1826+
'x-monite-version': '1.0.0',
1827+
},
1828+
path: {
1829+
entity_id: '1',
1830+
},
1831+
query: {
1832+
referer: 'https://example.com',
1833+
},
1834+
} as const;
1835+
1836+
describe('getMutationCache().find()', () => {
1837+
it('supports getMutationCache().find() with parameters filter', async () => {
1838+
const { qraft, queryClient } = createClient();
1839+
1840+
const { result: mutationResult } = renderHook(
1841+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
1842+
{
1843+
wrapper: (props) => (
1844+
<Providers {...props} queryClient={queryClient} />
1845+
),
1846+
}
1847+
);
1848+
1849+
act(() => {
1850+
mutationResult.current.mutate({
1851+
verification_document_back: 'back',
1852+
verification_document_front: 'front',
1853+
});
1854+
});
1855+
1856+
await waitFor(() => {
1857+
expect(mutationResult.current.data).toBeDefined();
1858+
});
1859+
1860+
const mutationCache =
1861+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
1862+
const foundMutation = mutationCache.find({
1863+
parameters,
1864+
});
1865+
1866+
expect(foundMutation).toBeDefined();
1867+
expect(foundMutation?.state.data).toEqual({
1868+
body: {
1869+
verification_document_back: 'back',
1870+
verification_document_front: 'front',
1871+
},
1872+
...parameters,
1873+
});
1874+
});
1875+
1876+
it('supports getMutationCache().find() with mutationKey filter', async () => {
1877+
const { qraft, queryClient } = createClient();
1878+
1879+
const { result: mutationResult } = renderHook(
1880+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
1881+
{
1882+
wrapper: (props) => (
1883+
<Providers {...props} queryClient={queryClient} />
1884+
),
1885+
}
1886+
);
1887+
1888+
act(() => {
1889+
mutationResult.current.mutate({
1890+
verification_document_back: 'back',
1891+
verification_document_front: 'front',
1892+
});
1893+
});
1894+
1895+
await waitFor(() => {
1896+
expect(mutationResult.current.data).toBeDefined();
1897+
});
1898+
1899+
const mutationCache =
1900+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
1901+
const foundMutation = mutationCache.find({
1902+
mutationKey:
1903+
qraft.entities.postEntitiesIdDocuments.getMutationKey(parameters),
1904+
});
1905+
1906+
expect(foundMutation).toBeDefined();
1907+
expect(foundMutation?.state.data).toEqual({
1908+
body: {
1909+
verification_document_back: 'back',
1910+
verification_document_front: 'front',
1911+
},
1912+
...parameters,
1913+
});
1914+
});
1915+
1916+
it('supports getMutationCache().find() with partial not exact parameters', async () => {
1917+
const { qraft, queryClient } = createClient();
1918+
1919+
const { result: mutationResult } = renderHook(
1920+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
1921+
{
1922+
wrapper: (props) => (
1923+
<Providers {...props} queryClient={queryClient} />
1924+
),
1925+
}
1926+
);
1927+
1928+
act(() => {
1929+
mutationResult.current.mutate({
1930+
verification_document_back: 'back',
1931+
verification_document_front: 'front',
1932+
});
1933+
});
1934+
1935+
await waitFor(() => {
1936+
expect(mutationResult.current.data).toBeDefined();
1937+
});
1938+
1939+
// test with partial parameters - header only
1940+
const mutationCache =
1941+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
1942+
const foundMutation = mutationCache.find({
1943+
exact: false,
1944+
parameters: {
1945+
header: parameters.header,
1946+
},
1947+
});
1948+
1949+
expect(foundMutation).toBeDefined();
1950+
expect(foundMutation?.state.data?.header?.['x-monite-version']).toEqual(
1951+
parameters.header['x-monite-version']
1952+
);
1953+
});
1954+
1955+
it('returns undefined when no matching mutation found', async () => {
1956+
const { qraft, queryClient } = createClient();
1957+
1958+
const { result: mutationResult } = renderHook(
1959+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
1960+
{
1961+
wrapper: (props) => (
1962+
<Providers {...props} queryClient={queryClient} />
1963+
),
1964+
}
1965+
);
1966+
1967+
act(() => {
1968+
mutationResult.current.mutate({
1969+
verification_document_back: 'back',
1970+
verification_document_front: 'front',
1971+
});
1972+
});
1973+
1974+
await waitFor(() => {
1975+
expect(mutationResult.current.data).toBeDefined();
1976+
});
1977+
1978+
// search with parameters that do not match the existing mutation
1979+
const mutationCache =
1980+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
1981+
1982+
const foundMutation = mutationCache.find({
1983+
parameters: {
1984+
...parameters,
1985+
path: {
1986+
entity_id: 'non-existing-id',
1987+
},
1988+
},
1989+
});
1990+
1991+
expect(foundMutation).toBeUndefined();
1992+
});
1993+
1994+
it('supports getMutationCache().find() with predicate function', async () => {
1995+
const { qraft, queryClient } = createClient();
1996+
1997+
const { result: mutationResult } = renderHook(
1998+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
1999+
{
2000+
wrapper: (props) => (
2001+
<Providers {...props} queryClient={queryClient} />
2002+
),
2003+
}
2004+
);
2005+
2006+
act(() => {
2007+
mutationResult.current.mutate({
2008+
verification_document_back: 'back',
2009+
verification_document_front: 'front',
2010+
});
2011+
});
2012+
2013+
await waitFor(() => {
2014+
expect(mutationResult.current.data).toBeDefined();
2015+
});
2016+
2017+
const mutationCache =
2018+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
2019+
const foundMutation = mutationCache.find({
2020+
predicate: (mutation) => {
2021+
return mutation.state.data?.header?.['x-monite-version'] === '1.0.0';
2022+
},
2023+
});
2024+
2025+
expect(foundMutation).toBeDefined();
2026+
expect(foundMutation?.state.data?.header?.['x-monite-version']).toEqual(
2027+
'1.0.0'
2028+
);
2029+
});
2030+
});
2031+
2032+
describe('getMutationCache().findAll()', () => {
2033+
it('supports getMutationCache().findAll() without filters', async () => {
2034+
const { qraft, queryClient } = createClient();
2035+
2036+
const { result: mutationResult } = renderHook(
2037+
() => qraft.entities.postEntitiesIdDocuments.useMutation(parameters),
2038+
{
2039+
wrapper: (props) => (
2040+
<Providers {...props} queryClient={queryClient} />
2041+
),
2042+
}
2043+
);
2044+
2045+
act(() => {
2046+
mutationResult.current.mutate({
2047+
verification_document_back: 'back',
2048+
verification_document_front: 'front',
2049+
});
2050+
});
2051+
2052+
await waitFor(() => {
2053+
expect(mutationResult.current.data).toBeDefined();
2054+
});
2055+
2056+
const mutationCache =
2057+
qraft.entities.postEntitiesIdDocuments.getMutationCache();
2058+
const [foundMutation] = mutationCache.findAll();
2059+
2060+
expect(foundMutation).toBeDefined();
2061+
expect(foundMutation?.state.data).toEqual({
2062+
body: {
2063+
verification_document_back: 'back',
2064+
verification_document_front: 'front',
2065+
},
2066+
...parameters,
2067+
});
2068+
});
2069+
});
2070+
});
2071+
18232072
describe('Qraft uses Operation Query Function', () => {
18242073
const parameters: Services['approvalPolicies']['getApprovalPoliciesId']['types']['parameters'] =
18252074
{

packages/tanstack-query-react-plugin/generate-ts-factory.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const serviceOperationFiles = [
2828
'ServiceOperationUseIsMutating.ts',
2929
'ServiceOperationUseMutation.ts',
3030
'ServiceOperationUseMutationState.ts',
31+
'ServiceOperationGetMutationCache.ts',
3132
'ServiceOperationUseQueries.ts',
3233
'ServiceOperationUseQuery.ts',
3334
'ServiceOperationUseSuspenseInfiniteQuery.ts',

0 commit comments

Comments
 (0)