Skip to content

Commit 82946b4

Browse files
authored
Fix useSWRInfinite pagination and header support (v8/master) (#2587)
1 parent c884b8b commit 82946b4

File tree

8 files changed

+313
-10
lines changed

8 files changed

+313
-10
lines changed

packages/swr/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"build": "tsdown --config-loader unrun",
1919
"dev": "tsdown --config-loader unrun --watch src",
2020
"lint": "eslint .",
21+
"test": "vitest",
2122
"clean": "rimraf .turbo dist",
2223
"nuke": "rimraf .turbo dist node_modules"
2324
},
@@ -28,7 +29,8 @@
2829
"devDependencies": {
2930
"eslint": "catalog:",
3031
"rimraf": "catalog:",
31-
"tsdown": "catalog:"
32+
"tsdown": "catalog:",
33+
"vitest": "catalog:"
3234
},
3335
"stableVersion": "8.0.0-rc.1"
3436
}

packages/swr/src/client.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { GetterProp } from '@orval/core';
2+
import { GetterPropType } from '@orval/core';
3+
import { describe, expect, it } from 'vitest';
4+
5+
describe('query parameter type extraction', () => {
6+
it('extracts type name from query param GetterProp', () => {
7+
const queryParamProp: GetterProp = {
8+
name: 'params',
9+
definition: 'params: ListPetsParams',
10+
implementation: 'params: ListPetsParams',
11+
default: false,
12+
required: true,
13+
type: GetterPropType.QUERY_PARAM,
14+
};
15+
16+
const props: GetterProp[] = [queryParamProp];
17+
const queryParam = props.find(
18+
(prop) => prop.type === GetterPropType.QUERY_PARAM,
19+
);
20+
const extractedType = queryParam?.definition.split(': ')[1] ?? 'never';
21+
22+
expect(extractedType).toBe('ListPetsParams');
23+
});
24+
25+
it('extracts type name from optional query param', () => {
26+
const queryParamProp: GetterProp = {
27+
name: 'params',
28+
definition: 'params?: GetUsersParams',
29+
implementation: 'params?: GetUsersParams',
30+
default: false,
31+
required: false,
32+
type: GetterPropType.QUERY_PARAM,
33+
};
34+
35+
const props: GetterProp[] = [queryParamProp];
36+
const queryParam = props.find(
37+
(prop) => prop.type === GetterPropType.QUERY_PARAM,
38+
);
39+
const extractedType = queryParam?.definition.split(': ')[1] ?? 'never';
40+
41+
expect(extractedType).toBe('GetUsersParams');
42+
});
43+
44+
it('returns never when no query param exists', () => {
45+
const pathParamProp: GetterProp = {
46+
name: 'id',
47+
definition: 'id: string',
48+
implementation: 'id: string',
49+
default: false,
50+
required: true,
51+
type: GetterPropType.PARAM,
52+
};
53+
54+
const props: GetterProp[] = [pathParamProp];
55+
const queryParam = props.find(
56+
(prop) => prop.type === GetterPropType.QUERY_PARAM,
57+
);
58+
const extractedType = queryParam?.definition.split(': ')[1] ?? 'never';
59+
60+
expect(extractedType).toBe('never');
61+
});
62+
});

packages/swr/src/index.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { builder } from './index';
4+
5+
describe('swr builder', () => {
6+
it('returns a valid builder function', () => {
7+
const result = builder();
8+
expect(result).toBeDefined();
9+
expect(typeof result).toBe('function');
10+
});
11+
12+
it('builder returns client builder with required methods', () => {
13+
const result = builder()();
14+
expect(result).toBeDefined();
15+
expect(result.client).toBeDefined();
16+
expect(result.dependencies).toBeDefined();
17+
expect(result.header).toBeDefined();
18+
expect(typeof result.client).toBe('function');
19+
expect(typeof result.dependencies).toBe('function');
20+
expect(typeof result.header).toBe('function');
21+
});
22+
});

packages/swr/src/index.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ const generateSwrImplementation = ({
162162
props,
163163
doc,
164164
httpClient,
165+
pathOnlyParams,
166+
headerOnlyParams,
167+
hasQueryParams,
168+
queryParamType,
165169
}: {
166170
isRequestOptions: boolean;
167171
operationName: string;
@@ -176,6 +180,10 @@ const generateSwrImplementation = ({
176180
swrOptions: SwrOptions;
177181
doc?: string;
178182
httpClient: OutputHttpClient;
183+
pathOnlyParams: string;
184+
headerOnlyParams: string;
185+
hasQueryParams: boolean;
186+
queryParamType: string;
179187
}) => {
180188
const swrProps = toObjectString(props, 'implementation');
181189

@@ -192,7 +200,7 @@ const generateSwrImplementation = ({
192200
: ''
193201
}`;
194202
const swrKeyImplementation = `const swrKey = swrOptions?.swrKey ?? (() => isEnabled ? ${swrKeyFnName}(${swrKeyProperties}) : null);`;
195-
const swrKeyLoaderImplementation = `const swrKeyLoader = swrOptions?.swrKeyLoader ?? (() => isEnabled ? ${swrKeyLoaderFnName}(${swrKeyProperties}) : null);`;
203+
const swrKeyLoaderImplementation = `const swrKeyLoader = swrOptions?.swrKeyLoader ?? (isEnabled ? ${swrKeyLoaderFnName}(${swrKeyProperties}) : () => null);`;
196204

197205
const errorType = getSwrErrorType(response, httpClient, mutator);
198206
const swrRequestSecondArg = getSwrRequestSecondArg(httpClient, mutator);
@@ -223,9 +231,11 @@ ${doc}export const ${camel(
223231
224232
${enabledImplementation}
225233
${swrKeyLoaderImplementation}
226-
const swrFn = () => ${operationName}(${httpFunctionProps}${
227-
httpFunctionProps && httpRequestSecondArg ? ', ' : ''
228-
}${httpRequestSecondArg})
234+
const swrFn = ${
235+
hasQueryParams
236+
? `([_url, pageParams]: [string, ${queryParamType} & { page: number }]) => ${operationName}(${pathOnlyParams}${pathOnlyParams ? ', ' : ''}pageParams${headerOnlyParams ? ', ' + headerOnlyParams : ''}${httpRequestSecondArg ? ', ' + httpRequestSecondArg : ''})`
237+
: `([_url]: [string]) => ${operationName}(${pathOnlyParams}${headerOnlyParams ? (pathOnlyParams ? ', ' : '') + headerOnlyParams : ''}${httpRequestSecondArg ? (pathOnlyParams || headerOnlyParams ? ', ' : '') + httpRequestSecondArg : ''})`
238+
}
229239
230240
const ${queryResultVarName} = useSWRInfinite<Awaited<ReturnType<typeof swrFn>>, TError>(swrKeyLoader, swrFn, ${
231241
swrOptions.swrInfiniteOptions
@@ -406,7 +416,8 @@ const generateSwrHook = (
406416
(prop) =>
407417
prop.type === GetterPropType.PARAM ||
408418
prop.type === GetterPropType.QUERY_PARAM ||
409-
prop.type === GetterPropType.NAMED_PATH_PARAMS,
419+
prop.type === GetterPropType.NAMED_PATH_PARAMS ||
420+
prop.type === GetterPropType.HEADER,
410421
),
411422
'implementation',
412423
);
@@ -416,7 +427,8 @@ const generateSwrHook = (
416427
(prop) =>
417428
prop.type === GetterPropType.PARAM ||
418429
prop.type === GetterPropType.QUERY_PARAM ||
419-
prop.type === GetterPropType.NAMED_PATH_PARAMS,
430+
prop.type === GetterPropType.NAMED_PATH_PARAMS ||
431+
prop.type === GetterPropType.HEADER,
420432
)
421433
.map((param) => {
422434
return param.type === GetterPropType.NAMED_PATH_PARAMS
@@ -463,6 +475,35 @@ const generateSwrHook = (
463475
})
464476
.join(',');
465477

478+
// For useSWRInfinite: separate path params from query params
479+
const pathOnlyParams = props
480+
.filter(
481+
(prop) =>
482+
prop.type === GetterPropType.PARAM ||
483+
prop.type === GetterPropType.NAMED_PATH_PARAMS,
484+
)
485+
.map((param) => {
486+
return param.type === GetterPropType.NAMED_PATH_PARAMS
487+
? param.destructured
488+
: param.name;
489+
})
490+
.join(',');
491+
492+
const headerOnlyParams = props
493+
.filter((prop) => prop.type === GetterPropType.HEADER)
494+
.map((param) => param.name)
495+
.join(',');
496+
497+
const hasQueryParams = props.some(
498+
(prop) => prop.type === GetterPropType.QUERY_PARAM,
499+
);
500+
501+
// Extract just the type name from definition (e.g., "params: ListPetsParams" -> "ListPetsParams")
502+
const queryParamType =
503+
props
504+
.find((prop) => prop.type === GetterPropType.QUERY_PARAM)
505+
?.definition.split(': ')[1] ?? 'never';
506+
466507
const queryKeyProps = toObjectString(
467508
props.filter((prop) => prop.type !== GetterPropType.HEADER),
468509
'implementation',
@@ -480,7 +521,7 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
480521
);
481522
const swrKeyLoader = override.swr.useInfinite
482523
? `export const ${swrKeyLoaderFnName} = (${queryKeyProps}) => {
483-
return (page: number, previousPageData: Awaited<ReturnType<typeof ${operationName}>>) => {
524+
return (page: number, previousPageData?: Awaited<ReturnType<typeof ${operationName}>>) => {
484525
if (previousPageData && !previousPageData.data) return null
485526
486527
return [\`${route}\`${queryParams ? ', ...(params ? [{...params,page}]: [{page}])' : ''}${
@@ -504,14 +545,18 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
504545
swrOptions: override.swr,
505546
doc,
506547
httpClient,
548+
pathOnlyParams,
549+
headerOnlyParams,
550+
hasQueryParams,
551+
queryParamType,
507552
});
508553

509554
if (!override.swr.useSWRMutationForGet) {
510555
return swrKeyFn + swrKeyLoader + swrImplementation;
511556
}
512557

513558
// For OutputClient.SWR_GET_MUTATION, generate both useSWR and useSWRMutation
514-
const httpFnPropertiesForGet = props
559+
const httpFnPropertiesForGetWithoutHeaders = props
515560
.filter((prop) => prop.type !== GetterPropType.HEADER)
516561
.map((prop) => {
517562
return prop.type === GetterPropType.NAMED_PATH_PARAMS
@@ -520,6 +565,18 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
520565
})
521566
.join(', ');
522567

568+
const headerParamsForGet = props
569+
.filter((prop) => prop.type === GetterPropType.HEADER)
570+
.map((param) => param.name)
571+
.join(', ');
572+
573+
const httpFnPropertiesForGet = [
574+
httpFnPropertiesForGetWithoutHeaders,
575+
headerParamsForGet,
576+
]
577+
.filter(Boolean)
578+
.join(', ');
579+
523580
const swrMutationFetcherType = getSwrMutationFetcherType(
524581
response,
525582
httpClient,
@@ -575,7 +632,7 @@ export const ${swrMutationFetcherName} = (${queryKeyProps} ${swrMutationFetcherO
575632
swrMutationImplementation
576633
);
577634
} else {
578-
const httpFnProperties = props
635+
const httpFnPropertiesWithoutHeaders = props
579636
.filter((prop) => prop.type !== GetterPropType.HEADER)
580637
.map((prop) => {
581638
if (prop.type === GetterPropType.NAMED_PATH_PARAMS) {
@@ -588,6 +645,15 @@ export const ${swrMutationFetcherName} = (${queryKeyProps} ${swrMutationFetcherO
588645
})
589646
.join(', ');
590647

648+
const headerParams = props
649+
.filter((prop) => prop.type === GetterPropType.HEADER)
650+
.map((param) => param.name)
651+
.join(', ');
652+
653+
const httpFnProperties = [httpFnPropertiesWithoutHeaders, headerParams]
654+
.filter(Boolean)
655+
.join(', ');
656+
591657
const swrKeyFnName = camel(`get-${operationName}-mutation-key`);
592658
const swrMutationKeyFn = `export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
593659
queryParams ? ', ...(params ? [params]: [])' : ''

packages/swr/vitest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({});

tests/configs/swr.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,22 @@ export default defineConfig({
186186
target: '../specifications/petstore.yaml',
187187
},
188188
},
189+
petstoreWithHeaders: {
190+
output: {
191+
target: '../generated/swr/petstore-with-headers/endpoints.ts',
192+
schemas: '../generated/swr/petstore-with-headers/model',
193+
client: 'swr',
194+
headers: true,
195+
override: {
196+
swr: {
197+
useInfinite: true,
198+
},
199+
},
200+
},
201+
input: {
202+
target: '../specifications/petstore.yaml',
203+
},
204+
},
189205
blobFile: {
190206
output: {
191207
target: '../generated/swr/blob-file/endpoints.ts',

0 commit comments

Comments
 (0)