Skip to content

Commit eca659e

Browse files
committed
feat: inline useQuery in generated hooks to preserve type inference
1 parent 549a42e commit eca659e

File tree

10 files changed

+144
-38
lines changed

10 files changed

+144
-38
lines changed

packages/openapi-ts-tests/main/test/plugins.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ for (const version of versions) {
6464
}),
6565
description: 'generate Fetch API client with TanStack React Query plugin',
6666
},
67+
{
68+
config: createConfig({
69+
output: 'useQuery-disabled',
70+
plugins: [
71+
{
72+
name: '@tanstack/react-query',
73+
useQuery: false,
74+
},
75+
'@hey-api/client-fetch',
76+
],
77+
}),
78+
description:
79+
'generate Fetch API client with TanStack React Query plugin with useQuery disabled',
80+
},
6781
{
6882
config: createConfig({
6983
output: 'fetch',

packages/openapi-ts/src/plugins/@tanstack/preact-query/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const defaultConfig: TanStackPreactQueryPlugin['Config'] = {
8585
plugin.config.useQuery = context.valueToObject({
8686
defaultValue: {
8787
case: plugin.config.case ?? 'camelCase',
88-
enabled: false,
88+
enabled: true,
8989
name: 'use{{name}}Query',
9090
},
9191
mappers: {

packages/openapi-ts/src/plugins/@tanstack/preact-query/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export type UserConfig = Plugin.Name<'@tanstack/preact-query'> &
339339
* - `string` or `function`: Shorthand for `{ name: string | function }`
340340
* - `object`: Full configuration object
341341
*
342-
* @default false
342+
* @default true
343343
*/
344344
useQuery?:
345345
| boolean

packages/openapi-ts/src/plugins/@tanstack/query-core/v5/infiniteQueryOptions.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export const createInfiniteQueryOptions = ({
139139
const typeData = useTypeData({ operation, plugin });
140140
const typeResponse = useTypeResponse({ operation, plugin });
141141

142+
const symbolSkipToken = $(plugin.external(`${plugin.name}.skipToken`));
143+
144+
const optsName = 'opts';
145+
142146
const symbolQueryKeyType = plugin.referenceSymbol({
143147
category: 'type',
144148
resource: 'QueryKey',
@@ -180,7 +184,7 @@ export const createInfiniteQueryOptions = ({
180184
)
181185
.call(
182186
$.object()
183-
.spread('options')
187+
.spread(optsName)
184188
.spread('params')
185189
.prop('signal', $('signal'))
186190
.prop('throwOnError', $.literal(true)),
@@ -216,6 +220,13 @@ export const createInfiniteQueryOptions = ({
216220
statements.push($.const().object('data').assign(awaitSdkFn), $.return('data'));
217221
}
218222

223+
const asyncQueryFn = $.func()
224+
.async()
225+
.param((p) => p.object('pageParam', 'queryKey', 'signal'))
226+
.do(...statements);
227+
228+
const paramType = $.type.or(typeData, $.type.query(symbolSkipToken));
229+
219230
const symbolInfiniteQueryOptionsFn = plugin.symbol(
220231
applyNaming(operation.id, plugin.config.infiniteQueryOptions),
221232
);
@@ -224,26 +235,33 @@ export const createInfiniteQueryOptions = ({
224235
.$if(plugin.config.comments && createOperationComment(operation), (c, v) => c.doc(v))
225236
.assign(
226237
$.func()
227-
.param('options', (p) => p.required(isRequiredOptions).type(typeData))
238+
.param('options', (p) => p.required(isRequiredOptions).type(paramType))
228239
.do(
240+
$.const(optsName).assign(
241+
$.ternary($('options').neq(symbolSkipToken)).do($('options')).otherwise($('undefined')),
242+
),
229243
$.return(
230244
$(symbolInfiniteQueryOptions)
231245
.call(
232246
$.object()
233247
.pretty()
234-
.hint('@ts-ignore')
235248
.prop(
236249
'queryFn',
237-
$.func()
238-
.async()
239-
.param((p) => p.object('pageParam', 'queryKey', 'signal'))
240-
.do(...statements),
250+
$.ternary($('options').eq(symbolSkipToken))
251+
.do(symbolSkipToken)
252+
.otherwise(asyncQueryFn),
253+
)
254+
.prop(
255+
'queryKey',
256+
$(symbolInfiniteQueryKey).call(
257+
isRequiredOptions ? $(optsName).as(typeData) : $(optsName),
258+
),
241259
)
242-
.prop('queryKey', $(symbolInfiniteQueryKey).call('options'))
243260
.$if(handleMeta(plugin, operation, 'infiniteQueryOptions'), (o, v) =>
244261
o.prop('meta', v),
245262
),
246263
)
264+
.hint('@ts-ignore')
247265
.generics(
248266
typeResponse,
249267
useTypeError({ operation, plugin }),

packages/openapi-ts/src/plugins/@tanstack/query-core/v5/plugin.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import type { PluginHandler } from '../types';
1+
import type { PluginHandler, PluginInstance } from '../types';
22
import { createInfiniteQueryOptions } from './infiniteQueryOptions';
33
import { createMutationOptions } from './mutationOptions';
44
import { createQueryOptions } from './queryOptions';
55
import { createUseMutation } from './useMutation';
66
import { createUseQuery } from './useQuery';
77

8+
const createQueryStyleNames = new Set<PluginInstance['name']>([
9+
'@tanstack/angular-query-experimental',
10+
'@tanstack/solid-query',
11+
'@tanstack/svelte-query',
12+
]);
13+
14+
const getMutationOptionsType = (name: PluginInstance['name']) =>
15+
createQueryStyleNames.has(name) ? 'MutationOptions' : 'UseMutationOptions';
16+
17+
const getQueryOptionsType = (name: PluginInstance['name']) =>
18+
createQueryStyleNames.has(name) ? 'CreateQueryOptions' : 'UseQueryOptions';
19+
820
export const handlerV5: PluginHandler = ({ plugin }) => {
921
plugin.symbol('DefaultError', {
1022
external: plugin.name,
@@ -14,13 +26,7 @@ export const handlerV5: PluginHandler = ({ plugin }) => {
1426
external: plugin.name,
1527
kind: 'type',
1628
});
17-
const mutationsType =
18-
plugin.name === '@tanstack/angular-query-experimental' ||
19-
plugin.name === '@tanstack/svelte-query' ||
20-
plugin.name === '@tanstack/solid-query'
21-
? 'MutationOptions'
22-
: 'UseMutationOptions';
23-
plugin.symbol(mutationsType, {
29+
plugin.symbol(getMutationOptionsType(plugin.name), {
2430
external: plugin.name,
2531
kind: 'type',
2632
meta: {
@@ -39,6 +45,22 @@ export const handlerV5: PluginHandler = ({ plugin }) => {
3945
plugin.symbol('useQuery', {
4046
external: plugin.name,
4147
});
48+
plugin.symbol(getQueryOptionsType(plugin.name), {
49+
external: plugin.name,
50+
kind: 'type',
51+
meta: {
52+
category: 'external',
53+
resource: `${plugin.name}.QueryObserverOptions`,
54+
},
55+
});
56+
plugin.symbol('skipToken', {
57+
external: plugin.name,
58+
meta: {
59+
category: 'external',
60+
resource: `${plugin.name}.skipToken`,
61+
},
62+
});
63+
4264
plugin.symbol('AxiosError', {
4365
external: 'axios',
4466
kind: 'type',

packages/openapi-ts/src/plugins/@tanstack/query-core/v5/queryOptions.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export const createQueryOptions = ({
5555

5656
const typeResponse = useTypeResponse({ operation, plugin });
5757

58+
const symbolSkipToken = $(plugin.external(`${plugin.name}.skipToken`));
59+
60+
const optsName = 'opts';
61+
5862
const awaitSdkFn = $.lazy((ctx) =>
5963
ctx
6064
.access(
@@ -66,7 +70,7 @@ export const createQueryOptions = ({
6670
)
6771
.call(
6872
$.object()
69-
.spread(optionsParamName)
73+
.spread(optsName)
7074
.spread($('queryKey').attr(0))
7175
.prop('signal', $('signal'))
7276
.prop('throwOnError', $.literal(true)),
@@ -81,17 +85,26 @@ export const createQueryOptions = ({
8185
statements.push($.const().object('data').assign(awaitSdkFn), $.return('data'));
8286
}
8387

88+
const asyncQueryFn = $.func()
89+
.async()
90+
.param((p) => p.object('queryKey', 'signal'))
91+
.do(...statements);
92+
93+
const typeData = useTypeData({ operation, plugin });
94+
95+
const optsForQueryKey = isRequiredOptions ? $(optsName).as(typeData) : $(optsName);
96+
8497
const queryOptionsObj = $.object()
8598
.pretty()
8699
.prop(
87100
'queryFn',
88-
$.func()
89-
.async()
90-
.param((p) => p.object('queryKey', 'signal'))
91-
.do(...statements),
101+
$.ternary($(optionsParamName).eq(symbolSkipToken))
102+
.do(symbolSkipToken)
103+
.otherwise(asyncQueryFn),
92104
)
93-
.prop('queryKey', $(symbolQueryKey).call(optionsParamName))
105+
.prop('queryKey', $(symbolQueryKey).call(optsForQueryKey))
94106
.$if(handleMeta(plugin, operation, 'queryOptions'), (o, v) => o.prop('meta', v));
107+
const paramType = $.type.or(typeData, $.type.query(symbolSkipToken));
95108

96109
const symbolQueryOptionsFn = plugin.symbol(
97110
applyNaming(operation.id, plugin.config.queryOptions),
@@ -112,10 +125,13 @@ export const createQueryOptions = ({
112125
.$if(plugin.config.comments && createOperationComment(operation), (c, v) => c.doc(v))
113126
.assign(
114127
$.func()
115-
.param(optionsParamName, (p) =>
116-
p.required(isRequiredOptions).type(useTypeData({ operation, plugin })),
117-
)
128+
.param(optionsParamName, (p) => p.required(isRequiredOptions).type(paramType))
118129
.do(
130+
$.const(optsName).assign(
131+
$.ternary($(optionsParamName).neq(symbolSkipToken))
132+
.do($(optionsParamName))
133+
.otherwise($('undefined')),
134+
),
119135
$(symbolQueryOptions)
120136
.call(queryOptionsObj)
121137
.generics(

packages/openapi-ts/src/plugins/@tanstack/query-core/v5/useQuery.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { useTypeData } from '../shared/useType';
1111
import type { PluginInstance } from '../types';
1212

1313
const optionsParamName = 'options';
14+
const queryOptionsKey = 'queryOptions';
15+
const sdkOptionsName = 'sdkOptions';
1416

1517
export const createUseQuery = ({
1618
operation,
@@ -27,30 +29,60 @@ export const createUseQuery = ({
2729
return;
2830
}
2931

30-
const symbolUseQueryFn = plugin.symbol(applyNaming(operation.id, plugin.config.useQuery));
31-
3232
const symbolUseQuery = plugin.external(`${plugin.name}.useQuery`);
33+
const symbolUseQueryFn = plugin.symbol(applyNaming(operation.id, plugin.config.useQuery));
3334

3435
const isRequiredOptions = isOperationOptionsRequired({
3536
context: plugin.context,
3637
operation,
3738
});
3839
const typeData = useTypeData({ operation, plugin });
3940

41+
const symbolSkipToken = $(plugin.external(`${plugin.name}.skipToken`));
42+
const sdkParamType = $.type.or(typeData, $.type.query(symbolSkipToken));
43+
4044
const symbolQueryOptionsFn = plugin.referenceSymbol({
4145
category: 'hook',
4246
resource: 'operation',
4347
resourceId: operation.id,
4448
role: 'queryOptions',
4549
tool: plugin.name,
4650
});
51+
52+
const queryOptionsType = $.type('Partial').generic(
53+
$.type('Omit', (t) =>
54+
t.generics(
55+
$(symbolQueryOptionsFn).returnType(),
56+
$.type.or($.type.literal('queryKey'), $.type.literal('queryFn')),
57+
),
58+
),
59+
);
60+
61+
const mergedParamType = $.type.and(
62+
sdkParamType,
63+
$.type.object().prop(queryOptionsKey, (p) => p.optional().type(queryOptionsType)),
64+
);
65+
66+
const func = $.func().param(optionsParamName, (p) =>
67+
p.required(isRequiredOptions).type(mergedParamType),
68+
);
69+
70+
func.do(
71+
$.if($(optionsParamName).eq(symbolSkipToken)).do(
72+
$(symbolUseQuery).call($(symbolQueryOptionsFn).call(optionsParamName)).return(),
73+
),
74+
$.const()
75+
.object(queryOptionsKey)
76+
.spread(sdkOptionsName)
77+
.assign(isRequiredOptions ? $(optionsParamName) : $(optionsParamName).coalesce($.object())),
78+
$(symbolUseQuery)
79+
.call($.object().spread($(symbolQueryOptionsFn).call(sdkOptionsName)).spread(queryOptionsKey))
80+
.return(),
81+
);
82+
4783
const statement = $.const(symbolUseQueryFn)
4884
.export()
4985
.$if(plugin.config.comments && createOperationComment(operation), (c, v) => c.doc(v))
50-
.assign(
51-
$.func()
52-
.param(optionsParamName, (p) => p.required(isRequiredOptions).type(typeData))
53-
.do($(symbolUseQuery).call($(symbolQueryOptionsFn).call(optionsParamName)).return()),
54-
);
86+
.assign(func);
5587
plugin.node(statement);
5688
};

packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const defaultConfig: TanStackReactQueryPlugin['Config'] = {
8585
plugin.config.useQuery = context.valueToObject({
8686
defaultValue: {
8787
case: plugin.config.case ?? 'camelCase',
88-
enabled: false,
88+
enabled: true,
8989
name: 'use{{name}}Query',
9090
},
9191
mappers: {

packages/openapi-ts/src/plugins/@tanstack/react-query/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export type UserConfig = Plugin.Name<'@tanstack/react-query'> &
339339
* - `string` or `function`: Shorthand for `{ name: string | function }`
340340
* - `object`: Full configuration object
341341
*
342-
* @default false
342+
* @default true
343343
*/
344344
useQuery?:
345345
| boolean

packages/openapi-ts/src/ts-dsl/expr/call.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { TsDsl } from '../base';
77
import { ArgsMixin } from '../mixins/args';
88
import { AsMixin } from '../mixins/as';
99
import { ExprMixin } from '../mixins/expr';
10+
import { HintMixin } from '../mixins/hint';
1011
import { SpreadMixin } from '../mixins/spread';
1112
import { TypeArgsMixin } from '../mixins/type-args';
1213
import { f } from '../utils/factories';
@@ -15,7 +16,9 @@ export type CallArgs = ReadonlyArray<CallCallee | undefined>;
1516
export type CallCallee = NodeName | MaybeTsDsl<ts.Expression>;
1617
export type CallCtor = (callee: CallCallee, ...args: CallArgs) => CallTsDsl;
1718

18-
const Mixed = ArgsMixin(AsMixin(ExprMixin(SpreadMixin(TypeArgsMixin(TsDsl<ts.CallExpression>)))));
19+
const Mixed = ArgsMixin(
20+
AsMixin(ExprMixin(HintMixin(SpreadMixin(TypeArgsMixin(TsDsl<ts.CallExpression>))))),
21+
);
1922

2023
export class CallTsDsl extends Mixed {
2124
readonly '~dsl' = 'CallTsDsl';
@@ -34,11 +37,12 @@ export class CallTsDsl extends Mixed {
3437
}
3538

3639
override toAst() {
37-
return ts.factory.createCallExpression(
40+
const node = ts.factory.createCallExpression(
3841
this.$node(this._callee),
3942
this.$generics(),
4043
this.$args(),
4144
);
45+
return this.$hint(node);
4246
}
4347
}
4448

0 commit comments

Comments
 (0)