Skip to content
Merged
4 changes: 3 additions & 1 deletion packages/swr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build": "tsdown --config-loader unconfig",
"dev": "tsdown --config-loader unconfig --watch src",
"lint": "eslint .",
"test": "vitest",
"clean": "rimraf .turbo dist",
"nuke": "rimraf .turbo dist node_modules"
},
Expand All @@ -26,6 +27,7 @@
"devDependencies": {
"eslint": "^9.38.0",
"rimraf": "^6.0.1",
"tsdown": "^0.15.8"
"tsdown": "^0.15.8",
"vitest": "^3.2.4"
}
}
62 changes: 62 additions & 0 deletions packages/swr/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { GetterProp } from '@orval/core';
import { GetterPropType } from '@orval/core';
import { describe, expect, it } from 'vitest';

describe('query parameter type extraction', () => {
it('extracts type name from query param GetterProp', () => {
const queryParamProp: GetterProp = {
name: 'params',
definition: 'params: ListPetsParams',
implementation: 'params: ListPetsParams',
default: false,
required: true,
type: GetterPropType.QUERY_PARAM,
};

const props: GetterProp[] = [queryParamProp];
const queryParam = props.find(
(prop) => prop.type === GetterPropType.QUERY_PARAM,
);
const extractedType = queryParam?.definition.split(': ')[1] || 'never';

expect(extractedType).toBe('ListPetsParams');
});

it('extracts type name from optional query param', () => {
const queryParamProp: GetterProp = {
name: 'params',
definition: 'params?: GetUsersParams',
implementation: 'params?: GetUsersParams',
default: false,
required: false,
type: GetterPropType.QUERY_PARAM,
};

const props: GetterProp[] = [queryParamProp];
const queryParam = props.find(
(prop) => prop.type === GetterPropType.QUERY_PARAM,
);
const extractedType = queryParam?.definition.split(': ')[1] || 'never';

expect(extractedType).toBe('GetUsersParams');
});

it('returns never when no query param exists', () => {
const pathParamProp: GetterProp = {
name: 'id',
definition: 'id: string',
implementation: 'id: string',
default: false,
required: true,
type: GetterPropType.PARAM,
};

const props: GetterProp[] = [pathParamProp];
const queryParam = props.find(
(prop) => prop.type === GetterPropType.QUERY_PARAM,
);
const extractedType = queryParam?.definition.split(': ')[1] || 'never';

expect(extractedType).toBe('never');
});
});
22 changes: 22 additions & 0 deletions packages/swr/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';

import { builder } from './index';

describe('swr builder', () => {
it('returns a valid builder function', () => {
const result = builder();
expect(result).toBeDefined();
expect(typeof result).toBe('function');
});

it('builder returns client builder with required methods', () => {
const result = builder()();
expect(result).toBeDefined();
expect(result.client).toBeDefined();
expect(result.dependencies).toBeDefined();
expect(result.header).toBeDefined();
expect(typeof result.client).toBe('function');
expect(typeof result.dependencies).toBe('function');
expect(typeof result.header).toBe('function');
});
});
84 changes: 75 additions & 9 deletions packages/swr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ const generateSwrImplementation = ({
props,
doc,
httpClient,
pathOnlyParams,
headerOnlyParams,
hasQueryParams,
queryParamType,
}: {
isRequestOptions: boolean;
operationName: string;
Expand All @@ -175,6 +179,10 @@ const generateSwrImplementation = ({
swrOptions: SwrOptions;
doc?: string;
httpClient: OutputHttpClient;
pathOnlyParams: string;
headerOnlyParams: string;
hasQueryParams: boolean;
queryParamType: string;
}) => {
const swrProps = toObjectString(props, 'implementation');

Expand All @@ -191,7 +199,7 @@ const generateSwrImplementation = ({
: ''
}`;
const swrKeyImplementation = `const swrKey = swrOptions?.swrKey ?? (() => isEnabled ? ${swrKeyFnName}(${swrKeyProperties}) : null);`;
const swrKeyLoaderImplementation = `const swrKeyLoader = swrOptions?.swrKeyLoader ?? (() => isEnabled ? ${swrKeyLoaderFnName}(${swrKeyProperties}) : null);`;
const swrKeyLoaderImplementation = `const swrKeyLoader = swrOptions?.swrKeyLoader ?? (isEnabled ? ${swrKeyLoaderFnName}(${swrKeyProperties}) : () => null);`;

const errorType = getSwrErrorType(response, httpClient, mutator);
const swrRequestSecondArg = getSwrRequestSecondArg(httpClient, mutator);
Expand Down Expand Up @@ -222,9 +230,11 @@ ${doc}export const ${camel(

${enabledImplementation}
${swrKeyLoaderImplementation}
const swrFn = () => ${operationName}(${httpFunctionProps}${
httpFunctionProps && httpRequestSecondArg ? ', ' : ''
}${httpRequestSecondArg})
const swrFn = ${
hasQueryParams
? `([_url, pageParams]: [string, ${queryParamType} & { page: number }]) => ${operationName}(${pathOnlyParams}${pathOnlyParams ? ', ' : ''}pageParams${headerOnlyParams ? ', ' + headerOnlyParams : ''}${httpRequestSecondArg ? ', ' + httpRequestSecondArg : ''})`
: `([_url]: [string]) => ${operationName}(${pathOnlyParams}${headerOnlyParams ? (pathOnlyParams ? ', ' : '') + headerOnlyParams : ''}${httpRequestSecondArg ? (pathOnlyParams || headerOnlyParams ? ', ' : '') + httpRequestSecondArg : ''})`
}

const ${queryResultVarName} = useSWRInfinite<Awaited<ReturnType<typeof swrFn>>, TError>(swrKeyLoader, swrFn, ${
swrOptions.swrInfiniteOptions
Expand Down Expand Up @@ -405,7 +415,8 @@ const generateSwrHook = (
(prop) =>
prop.type === GetterPropType.PARAM ||
prop.type === GetterPropType.QUERY_PARAM ||
prop.type === GetterPropType.NAMED_PATH_PARAMS,
prop.type === GetterPropType.NAMED_PATH_PARAMS ||
prop.type === GetterPropType.HEADER,
),
'implementation',
);
Expand All @@ -415,7 +426,8 @@ const generateSwrHook = (
(prop) =>
prop.type === GetterPropType.PARAM ||
prop.type === GetterPropType.QUERY_PARAM ||
prop.type === GetterPropType.NAMED_PATH_PARAMS,
prop.type === GetterPropType.NAMED_PATH_PARAMS ||
prop.type === GetterPropType.HEADER,
)
.map((param) => {
if (param.type === GetterPropType.NAMED_PATH_PARAMS) {
Expand Down Expand Up @@ -466,6 +478,35 @@ const generateSwrHook = (
})
.join(',');

// For useSWRInfinite: separate path params from query params
const pathOnlyParams = props
.filter(
(prop) =>
prop.type === GetterPropType.PARAM ||
prop.type === GetterPropType.NAMED_PATH_PARAMS,
)
.map((param) => {
return param.type === GetterPropType.NAMED_PATH_PARAMS
? param.destructured
: param.name;
})
.join(',');

const headerOnlyParams = props
.filter((prop) => prop.type === GetterPropType.HEADER)
.map((param) => param.name)
.join(',');

const hasQueryParams = props.some(
(prop) => prop.type === GetterPropType.QUERY_PARAM,
);

// Extract just the type name from definition (e.g., "params: ListPetsParams" -> "ListPetsParams")
const queryParamType =
props
.find((prop) => prop.type === GetterPropType.QUERY_PARAM)
?.definition.split(': ')[1] || 'never';

const queryKeyProps = toObjectString(
props.filter((prop) => prop.type !== GetterPropType.HEADER),
'implementation',
Expand All @@ -483,7 +524,7 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
);
const swrKeyLoader = override.swr.useInfinite
? `export const ${swrKeyLoaderFnName} = (${queryKeyProps}) => {
return (page: number, previousPageData: Awaited<ReturnType<typeof ${operationName}>>) => {
return (page: number, previousPageData?: Awaited<ReturnType<typeof ${operationName}>>) => {
if (previousPageData && !previousPageData.data) return null

return [\`${route}\`${queryParams ? ', ...(params ? [{...params,page}]: [{page}])' : ''}${
Expand All @@ -507,14 +548,18 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
swrOptions: override.swr,
doc,
httpClient,
pathOnlyParams,
headerOnlyParams,
hasQueryParams,
queryParamType,
});

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

// For OutputClient.SWR_GET_MUTATION, generate both useSWR and useSWRMutation
const httpFnPropertiesForGet = props
const httpFnPropertiesForGetWithoutHeaders = props
.filter((prop) => prop.type !== GetterPropType.HEADER)
.map((prop) => {
if (prop.type === GetterPropType.NAMED_PATH_PARAMS) {
Expand All @@ -525,6 +570,18 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
})
.join(', ');

const headerParamsForGet = props
.filter((prop) => prop.type === GetterPropType.HEADER)
.map((param) => param.name)
.join(', ');

const httpFnPropertiesForGet = [
httpFnPropertiesForGetWithoutHeaders,
headerParamsForGet,
]
.filter(Boolean)
.join(', ');

const swrMutationFetcherType = getSwrMutationFetcherType(
response,
httpClient,
Expand Down Expand Up @@ -580,7 +637,7 @@ export const ${swrMutationFetcherName} = (${queryKeyProps} ${swrMutationFetcherO
swrMutationImplementation
);
} else {
const httpFnProperties = props
const httpFnPropertiesWithoutHeaders = props
.filter((prop) => prop.type !== GetterPropType.HEADER)
.map((prop) => {
if (prop.type === GetterPropType.NAMED_PATH_PARAMS) {
Expand All @@ -593,6 +650,15 @@ export const ${swrMutationFetcherName} = (${queryKeyProps} ${swrMutationFetcherO
})
.join(', ');

const headerParams = props
.filter((prop) => prop.type === GetterPropType.HEADER)
.map((param) => param.name)
.join(', ');

const httpFnProperties = [httpFnPropertiesWithoutHeaders, headerParams]
.filter(Boolean)
.join(', ');

const swrKeyFnName = camel(`get-${operationName}-mutation-key`);
const swrMutationKeyFn = `export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
queryParams ? ', ...(params ? [params]: [])' : ''
Expand Down
3 changes: 3 additions & 0 deletions packages/swr/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({});
16 changes: 16 additions & 0 deletions tests/configs/swr.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@ export default defineConfig({
target: '../specifications/petstore.yaml',
},
},
petstoreWithHeaders: {
output: {
target: '../generated/swr/petstore-with-headers/endpoints.ts',
schemas: '../generated/swr/petstore-with-headers/model',
client: 'swr',
headers: true,
override: {
swr: {
useInfinite: true,
},
},
},
input: {
target: '../specifications/petstore.yaml',
},
},
blobFile: {
output: {
target: '../generated/swr/blob-file/endpoints.ts',
Expand Down
Loading