Skip to content

Commit 4e0163e

Browse files
committed
fix(query): skip set/get-query-data helpers under hook-shaped queryOptions mutator
When `queryOptions` is a hook-based mutator the query hook caches under a mutated queryKey, but the generated `useSet*QueryData` / `useGet*QueryData` helpers cannot legally call that hook to recover the key. Emitting them would silently target a different cache slot than the query hook writes to. Skip generation of those helpers when `queryOptionsMutator.isHook` is true and surface a warning so the misconfiguration is visible. `invalidate` is left unchanged for backwards compatibility (same gap, pre-existing). Also pin the `allowUndefinedParam` → `wrapPropsBodyWithMutatorBodyType` composition with a unit test that exercises the optional-body pipeline used by `set*QueryData`. Addresses CodeRabbit review feedback on #3363
1 parent b5e5bdc commit 4e0163e

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

packages/query/src/query-generator.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,22 @@ describe('allowUndefinedParam', () => {
337337
it('returns empty input unchanged', () => {
338338
expect(allowUndefinedParam('')).toBe('');
339339
});
340+
341+
// Regression for the pipeline that drives `set*QueryData` props: an
342+
// optional body prop is widened to `body: T | undefined`, then
343+
// `wrapPropsBodyWithMutatorBodyType` must still wrap the body type as
344+
// `BodyType<T>`. If the union order ever swaps to `undefined | T`, the
345+
// wrap regex would still match — but verify the happy path explicitly so
346+
// future regex tweaks cannot break body wrapping silently.
347+
it('composes with wrapPropsBodyWithMutatorBodyType for optional body params', () => {
348+
const widened = allowUndefinedParam('createPetsBody?: CreatePetsBody');
349+
expect(widened).toBe('createPetsBody: CreatePetsBody | undefined');
350+
expect(
351+
wrapPropsBodyWithMutatorBodyType({
352+
propsString: widened,
353+
body: { definition: 'CreatePetsBody' } as unknown as GetterBody,
354+
mutator: { bodyTypeName: 'BodyType' } as unknown as GeneratorMutator,
355+
}),
356+
).toBe('createPetsBody: BodyType<CreatePetsBody> | undefined');
357+
});
340358
});

packages/query/src/query-generator.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -716,9 +716,10 @@ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${q
716716
// queryOptions mutator may augment the queryKey (e.g. tenant prefix).
717717
// Route invalidate / set / get helpers through the mutator so the key
718718
// matches what the query hook actually wrote into the cache. Hook-shaped
719-
// mutators are skipped uniformly: invalidate / non-React set / non-React
720-
// get are plain functions and cannot call hooks, and the React set / get
721-
// helpers follow the same gate so all four helpers target the same key.
719+
// mutators are skipped here because none of these helpers can legally
720+
// call a hook at the right time — set/get helpers stop being emitted
721+
// entirely in that case (see `hasHookMutator` below), and invalidate
722+
// falls back to the unmutated base key for backwards compatibility.
722723
const applyQueryOptionsMutator = (baseExpr: string) =>
723724
queryOptionsMutator && !queryOptionsMutator.isHook
724725
? `${queryOptionsMutator.name}({ queryKey: ${baseExpr} }${
@@ -728,13 +729,28 @@ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${q
728729
}).queryKey`
729730
: baseExpr;
730731

732+
// Hook-shaped queryOptions mutators rewrite the queryKey at hook-call
733+
// time, but the set/get helpers cannot call a hook to recover that key.
734+
// Emitting them would silently target a different cache slot than the
735+
// query hook actually wrote into, so we skip generation in that case and
736+
// surface a warning to the user. Invalidate is left alone for backwards
737+
// compatibility — it has shipped with the same gap and changing its
738+
// contract is out of scope here.
739+
const hasHookMutator = !!queryOptionsMutator?.isHook;
740+
if (hasHookMutator && (useSetQueryData || useGetQueryData)) {
741+
logWarning(
742+
`'${name}' has a hook-based queryOptions mutator, so the requested set/get-query-data helpers were skipped to avoid a cache-key mismatch with the query hook.`,
743+
);
744+
}
745+
731746
const shouldGenerateInvalidate = useInvalidate && isPrimaryQueryType;
732747
const invalidateFnName = camel(`invalidate-${name}`);
733748
const invalidateQueryKeyExpr = applyQueryOptionsMutator(
734749
buildBaseQueryKeyExpr(),
735750
);
736751

737-
const shouldGenerateSetQueryData = useSetQueryData && isPrimaryQueryType;
752+
const shouldGenerateSetQueryData =
753+
useSetQueryData && isPrimaryQueryType && !hasHookMutator;
738754
const isReactQuery = adapter.outputClient === OutputClient.REACT_QUERY;
739755
const setQueryDataFnName = isReactQuery
740756
? camel(`use-set-${name}-query-data`)
@@ -756,7 +772,8 @@ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${q
756772
widenNonPath: allowUndefinedParam,
757773
});
758774

759-
const shouldGenerateGetQueryData = useGetQueryData && isPrimaryQueryType;
775+
const shouldGenerateGetQueryData =
776+
useGetQueryData && isPrimaryQueryType && !hasHookMutator;
760777
const getQueryDataFnName = isReactQuery
761778
? camel(`use-get-${name}-query-data`)
762779
: camel(`get-${name}-query-data`);

0 commit comments

Comments
 (0)