Skip to content

Commit d066aee

Browse files
committed
feat(@tanstack/query): generate type-safe setQueryData helpers
Adds two opt-in config options to the @tanstack/* query plugins: - `setQueryData` (all 6 plugins): generates a plain function per query operation that takes a `QueryClient`, options, and an updater, and wires up the correct query key and response type. - `useSetQueryData` (react-query and preact-query only): generates a hook variant that calls `useQueryClient()` internally. Both are disabled by default and depend on `queryOptions` (enabled by default) - they reuse `xxxOptions(options).queryKey` to avoid emitting duplicate queryKey functions. Closes #3820
1 parent 8b02fcb commit d066aee

119 files changed

Lines changed: 28503 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": minor
3+
---
4+
5+
**plugin(@tanstack/*-query)**: add `setQueryData` config option that generates type-safe helpers wrapping `queryClient.setQueryData()` per query operation. Disabled by default.

packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/setQueryData/@tanstack/react-query.gen.ts

Lines changed: 529 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
import { type ClientOptions, type Config, createClient, createConfig } from './client';
4+
import type { ClientOptions as ClientOptions2 } from './types.gen';
5+
6+
/**
7+
* The `createClientConfig()` function will be called on client initialization
8+
* and the returned object will become the client's initial configuration.
9+
*
10+
* You may want to initialize your client this way instead of calling
11+
* `setConfig()`. This is useful for example if you're using Next.js
12+
* to ensure your client always has the correct values.
13+
*/
14+
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
15+
16+
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:3000/base' }));
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
import { createSseClient } from '../core/serverSentEvents.gen';
4+
import type { HttpMethod } from '../core/types.gen';
5+
import { getValidRequestBody } from '../core/utils.gen';
6+
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
7+
import {
8+
buildUrl,
9+
createConfig,
10+
createInterceptors,
11+
getParseAs,
12+
mergeConfigs,
13+
mergeHeaders,
14+
setAuthParams,
15+
} from './utils.gen';
16+
17+
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
18+
body?: any;
19+
headers: ReturnType<typeof mergeHeaders>;
20+
};
21+
22+
export const createClient = (config: Config = {}): Client => {
23+
let _config = mergeConfigs(createConfig(), config);
24+
25+
const getConfig = (): Config => ({ ..._config });
26+
27+
const setConfig = (config: Config): Config => {
28+
_config = mergeConfigs(_config, config);
29+
return getConfig();
30+
};
31+
32+
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
33+
34+
const beforeRequest = async <
35+
TData = unknown,
36+
TResponseStyle extends 'data' | 'fields' = 'fields',
37+
ThrowOnError extends boolean = boolean,
38+
Url extends string = string,
39+
>(
40+
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
41+
) => {
42+
const opts = {
43+
..._config,
44+
...options,
45+
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
46+
headers: mergeHeaders(_config.headers, options.headers),
47+
serializedBody: undefined as string | undefined,
48+
};
49+
50+
if (opts.security) {
51+
await setAuthParams({
52+
...opts,
53+
security: opts.security,
54+
});
55+
}
56+
57+
if (opts.requestValidator) {
58+
await opts.requestValidator(opts);
59+
}
60+
61+
if (opts.body !== undefined && opts.bodySerializer) {
62+
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
63+
}
64+
65+
// remove Content-Type header if body is empty to avoid sending invalid requests
66+
if (opts.body === undefined || opts.serializedBody === '') {
67+
opts.headers.delete('Content-Type');
68+
}
69+
70+
const resolvedOpts = opts as typeof opts &
71+
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
72+
const url = buildUrl(resolvedOpts);
73+
74+
return { opts: resolvedOpts, url };
75+
};
76+
77+
const request: Client['request'] = async (options) => {
78+
const { opts, url } = await beforeRequest(options);
79+
const requestInit: ReqInit = {
80+
redirect: 'follow',
81+
...opts,
82+
body: getValidRequestBody(opts),
83+
};
84+
85+
let request = new Request(url, requestInit);
86+
87+
for (const fn of interceptors.request.fns) {
88+
if (fn) {
89+
request = await fn(request, opts);
90+
}
91+
}
92+
93+
// fetch must be assigned here, otherwise it would throw the error:
94+
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
95+
const _fetch = opts.fetch!;
96+
let response: Response;
97+
98+
try {
99+
response = await _fetch(request);
100+
} catch (error) {
101+
// Handle fetch exceptions (AbortError, network errors, etc.)
102+
let finalError = error;
103+
104+
for (const fn of interceptors.error.fns) {
105+
if (fn) {
106+
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
107+
}
108+
}
109+
110+
finalError = finalError || ({} as unknown);
111+
112+
if (opts.throwOnError) {
113+
throw finalError;
114+
}
115+
116+
// Return error response
117+
return opts.responseStyle === 'data'
118+
? undefined
119+
: {
120+
error: finalError,
121+
request,
122+
response: undefined as any,
123+
};
124+
}
125+
126+
for (const fn of interceptors.response.fns) {
127+
if (fn) {
128+
response = await fn(response, request, opts);
129+
}
130+
}
131+
132+
const result = {
133+
request,
134+
response,
135+
};
136+
137+
if (response.ok) {
138+
const parseAs =
139+
(opts.parseAs === 'auto'
140+
? getParseAs(response.headers.get('Content-Type'))
141+
: opts.parseAs) ?? 'json';
142+
143+
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
144+
let emptyData: any;
145+
switch (parseAs) {
146+
case 'arrayBuffer':
147+
case 'blob':
148+
case 'text':
149+
emptyData = await response[parseAs]();
150+
break;
151+
case 'formData':
152+
emptyData = new FormData();
153+
break;
154+
case 'stream':
155+
emptyData = response.body;
156+
break;
157+
case 'json':
158+
default:
159+
emptyData = {};
160+
break;
161+
}
162+
return opts.responseStyle === 'data'
163+
? emptyData
164+
: {
165+
data: emptyData,
166+
...result,
167+
};
168+
}
169+
170+
let data: any;
171+
switch (parseAs) {
172+
case 'arrayBuffer':
173+
case 'blob':
174+
case 'formData':
175+
case 'text':
176+
data = await response[parseAs]();
177+
break;
178+
case 'json': {
179+
// Some servers return 200 with no Content-Length and empty body.
180+
// response.json() would throw; read as text and parse if non-empty.
181+
const text = await response.text();
182+
data = text ? JSON.parse(text) : {};
183+
break;
184+
}
185+
case 'stream':
186+
return opts.responseStyle === 'data'
187+
? response.body
188+
: {
189+
data: response.body,
190+
...result,
191+
};
192+
}
193+
194+
if (parseAs === 'json') {
195+
if (opts.responseValidator) {
196+
await opts.responseValidator(data);
197+
}
198+
199+
if (opts.responseTransformer) {
200+
data = await opts.responseTransformer(data);
201+
}
202+
}
203+
204+
return opts.responseStyle === 'data'
205+
? data
206+
: {
207+
data,
208+
...result,
209+
};
210+
}
211+
212+
const textError = await response.text();
213+
let jsonError: unknown;
214+
215+
try {
216+
jsonError = JSON.parse(textError);
217+
} catch {
218+
// noop
219+
}
220+
221+
const error = jsonError ?? textError;
222+
let finalError = error;
223+
224+
for (const fn of interceptors.error.fns) {
225+
if (fn) {
226+
finalError = (await fn(error, response, request, opts)) as string;
227+
}
228+
}
229+
230+
finalError = finalError || ({} as string);
231+
232+
if (opts.throwOnError) {
233+
throw finalError;
234+
}
235+
236+
// TODO: we probably want to return error and improve types
237+
return opts.responseStyle === 'data'
238+
? undefined
239+
: {
240+
error: finalError,
241+
...result,
242+
};
243+
};
244+
245+
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
246+
request({ ...options, method });
247+
248+
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
249+
const { opts, url } = await beforeRequest(options);
250+
return createSseClient({
251+
...opts,
252+
body: opts.body as BodyInit | null | undefined,
253+
headers: opts.headers as unknown as Record<string, string>,
254+
method,
255+
onRequest: async (url, init) => {
256+
let request = new Request(url, init);
257+
for (const fn of interceptors.request.fns) {
258+
if (fn) {
259+
request = await fn(request, opts);
260+
}
261+
}
262+
return request;
263+
},
264+
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
265+
url,
266+
});
267+
};
268+
269+
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
270+
271+
return {
272+
buildUrl: _buildUrl,
273+
connect: makeMethodFn('CONNECT'),
274+
delete: makeMethodFn('DELETE'),
275+
get: makeMethodFn('GET'),
276+
getConfig,
277+
head: makeMethodFn('HEAD'),
278+
interceptors,
279+
options: makeMethodFn('OPTIONS'),
280+
patch: makeMethodFn('PATCH'),
281+
post: makeMethodFn('POST'),
282+
put: makeMethodFn('PUT'),
283+
request,
284+
setConfig,
285+
sse: {
286+
connect: makeSseFn('CONNECT'),
287+
delete: makeSseFn('DELETE'),
288+
get: makeSseFn('GET'),
289+
head: makeSseFn('HEAD'),
290+
options: makeSseFn('OPTIONS'),
291+
patch: makeSseFn('PATCH'),
292+
post: makeSseFn('POST'),
293+
put: makeSseFn('PUT'),
294+
trace: makeSseFn('TRACE'),
295+
},
296+
trace: makeMethodFn('TRACE'),
297+
} as Client;
298+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
export type { Auth } from '../core/auth.gen';
4+
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
5+
export {
6+
formDataBodySerializer,
7+
jsonBodySerializer,
8+
urlSearchParamsBodySerializer,
9+
} from '../core/bodySerializer.gen';
10+
export { buildClientParams } from '../core/params.gen';
11+
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
12+
export { createClient } from './client.gen';
13+
export type {
14+
Client,
15+
ClientOptions,
16+
Config,
17+
CreateClientConfig,
18+
Options,
19+
RequestOptions,
20+
RequestResult,
21+
ResolvedRequestOptions,
22+
ResponseStyle,
23+
TDataShape,
24+
} from './types.gen';
25+
export { createConfig, mergeHeaders } from './utils.gen';

0 commit comments

Comments
 (0)