Skip to content

Commit 03001cb

Browse files
authored
feat: Support string type for pageParam and nextPageParam options (#149)
1 parent be5cd9d commit 03001cb

File tree

12 files changed

+344
-41
lines changed

12 files changed

+344
-41
lines changed

examples/nextjs-app/app/components/PaginatedPets.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function PaginatedPets() {
2222
</React.Fragment>
2323
))}
2424
</ul>
25-
{data?.pages.at(-1)?.nextPage && (
25+
{data?.pages.at(-1)?.meta?.next && (
2626
<button
2727
type="button"
2828
onClick={() => fetchNextPage()}

examples/nextjs-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "next build",
1010
"start": "next start",
1111
"lint": "next lint",
12-
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml -c axios --request ./request.ts --format=biome --lint=biome"
12+
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml -c axios --request ./request.ts --format=biome --lint=biome --nextPageParam=meta.next"
1313
},
1414
"dependencies": {
1515
"@tanstack/react-query": "^5.32.1",

examples/petstore.yaml

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,62 @@ paths:
176176
type: array
177177
items:
178178
$ref: '#/components/schemas/Pet'
179-
nextPage:
180-
type: integer
181-
format: int32
182-
minimum: 1
179+
meta:
180+
type: object
181+
properties:
182+
next:
183+
type: integer
184+
format: int32
185+
minimum: 1
186+
total:
187+
type: integer
188+
/cursor-based-pets:
189+
get:
190+
description: |
191+
Returns cursor-based pets from the system that the user has access to
192+
operationId: findCursorBasedPets
193+
parameters:
194+
- name: page
195+
in: query
196+
description: string to start from
197+
required: false
198+
schema:
199+
type: string
200+
- name: tags
201+
in: query
202+
description: tags to filter by
203+
required: false
204+
style: form
205+
schema:
206+
type: array
207+
items:
208+
type: string
209+
- name: limit
210+
in: query
211+
description: maximum number of results to return
212+
required: false
213+
schema:
214+
type: integer
215+
format: int32
216+
responses:
217+
'200':
218+
description: pet response
219+
content:
220+
application/json:
221+
schema:
222+
type: object
223+
properties:
224+
pets:
225+
type: array
226+
items:
227+
$ref: '#/components/schemas/Pet'
228+
meta:
229+
type: object
230+
properties:
231+
next:
232+
type: string
233+
total:
234+
type: integer
183235

184236
components:
185237
schemas:

examples/react-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev:mock": "prism mock ../petstore.yaml --dynamic",
1010
"build": "tsc && vite build",
1111
"preview": "vite preview",
12-
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml -c axios --request ./request.ts --format=biome --lint=biome",
12+
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml -c axios --request ./request.ts --format=biome --lint=biome --nextPageParam=meta.next",
1313
"test:generated": "tsc -p ./tsconfig.openapi.json --noEmit"
1414
},
1515
"dependencies": {

lefthook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pre-commit:
22
commands:
33
check:
44
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
5-
run: npx biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true ./ && git update-index --again
5+
run: npx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true ./ && git update-index --again
66
test:
77
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
88
run: npx vitest run

src/cli.mts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,7 @@ async function setupProgram() {
103103
"Name of the response parameter used for next page",
104104
"nextPage",
105105
)
106-
.option(
107-
"--initialPageParam <value>",
108-
"Initial page value to query",
109-
"initialPageParam",
110-
)
106+
.option("--initialPageParam <value>", "Initial page value to query", "1")
111107
.parse();
112108

113109
const options = program.opts<LimitedUserConfig>();

src/createUseQuery.mts

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MethodDeclaration } from "ts-morph";
1+
import type { MethodDeclaration, Symbol as TMSymbol } from "ts-morph";
22
import ts from "typescript";
33
import {
44
BuildCommonTypeName,
@@ -19,7 +19,10 @@ import { addJSDocToNode } from "./util.mjs";
1919
export const createApiResponseType = ({
2020
className,
2121
methodName,
22-
}: { className: string; methodName: string }) => {
22+
}: {
23+
className: string;
24+
methodName: string;
25+
}) => {
2326
/** Awaited<ReturnType<typeof myClass.myMethod>> */
2427
const awaitedResponseDataType = ts.factory.createTypeReferenceNode(
2528
ts.factory.createIdentifier("Awaited"),
@@ -44,7 +47,9 @@ export const createApiResponseType = ({
4447
const apiResponse = ts.factory.createTypeAliasDeclaration(
4548
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
4649
ts.factory.createIdentifier(
47-
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(methodName)}DefaultResponse`,
50+
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(
51+
methodName,
52+
)}DefaultResponse`,
4853
),
4954
undefined,
5055
awaitedResponseDataType,
@@ -146,7 +151,9 @@ export function createReturnTypeExport({
146151
return ts.factory.createTypeAliasDeclaration(
147152
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
148153
ts.factory.createIdentifier(
149-
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(methodName)}QueryResult`,
154+
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(
155+
methodName,
156+
)}QueryResult`,
150157
),
151158
[
152159
ts.factory.createTypeParameterDeclaration(
@@ -179,7 +186,11 @@ export function createQueryKeyExport({
179186
className,
180187
methodName,
181188
queryKey,
182-
}: { className: string; methodName: string; queryKey: string }) {
189+
}: {
190+
className: string;
191+
methodName: string;
192+
queryKey: string;
193+
}) {
183194
return ts.factory.createVariableStatement(
184195
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
185196
ts.factory.createVariableDeclarationList(
@@ -201,20 +212,56 @@ export function createQueryKeyExport({
201212
export function hookNameFromMethod({
202213
method,
203214
className,
204-
}: { method: MethodDeclaration; className: string }) {
215+
}: {
216+
method: MethodDeclaration;
217+
className: string;
218+
}) {
205219
const methodName = getNameFromMethod(method);
206220
return `use${className}${capitalizeFirstLetter(methodName)}`;
207221
}
208222

209223
export function createQueryKeyFromMethod({
210224
method,
211225
className,
212-
}: { method: MethodDeclaration; className: string }) {
226+
}: {
227+
method: MethodDeclaration;
228+
className: string;
229+
}) {
213230
const customHookName = hookNameFromMethod({ method, className });
214231
const queryKey = `${customHookName}Key`;
215232
return queryKey;
216233
}
217234

235+
/**
236+
* Extracts the type of the next page parameter from the given properties.
237+
*
238+
* @param properties The properties to search through.
239+
* @param nextPageParam The name of the next page parameter.
240+
* @returns The type of the next page parameter, if found.
241+
*/
242+
function findNextPageParamType(
243+
properties: TMSymbol[] | undefined,
244+
nextPageParam: string,
245+
): string | undefined {
246+
if (!properties) return undefined;
247+
248+
for (const property of properties) {
249+
if (property.getName() === nextPageParam) {
250+
return property?.getDeclarations()?.at(0)?.getType()?.getText();
251+
}
252+
253+
const type = property.getDeclarations().at(0)?.getType();
254+
const nestedProperties = type?.getProperties();
255+
256+
if (!type?.isObject() || type.isArray()) continue;
257+
258+
const result = findNextPageParamType(nestedProperties, nextPageParam);
259+
if (result) return result;
260+
}
261+
262+
return undefined;
263+
}
264+
218265
/**
219266
* Creates a custom hook for a query
220267
* @param queryString The type of query to use from react-query
@@ -260,6 +307,18 @@ export function createQueryHook({
260307
const responseDataTypeIdentifier =
261308
responseDataTypeRef.typeName as ts.Identifier;
262309

310+
const arg = method.getReturnType().getTypeArguments().at(0);
311+
312+
const nextPageParamTypePropetires = arg?.getProperties();
313+
314+
const nextPageParamType =
315+
arg?.isObject() && nextPageParam
316+
? findNextPageParamType(
317+
nextPageParamTypePropetires,
318+
nextPageParam.split(".").at(-1) ?? "",
319+
)
320+
: undefined;
321+
263322
const hookExport = ts.factory.createVariableStatement(
264323
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
265324
ts.factory.createVariableDeclarationList(
@@ -430,7 +489,11 @@ export function createQueryHook({
430489
"pageParam",
431490
),
432491
ts.factory.createKeywordTypeNode(
433-
ts.SyntaxKind.NumberKeyword,
492+
p.type?.getText() === "number"
493+
? ts.SyntaxKind
494+
.NumberKeyword
495+
: ts.SyntaxKind
496+
.StringKeyword,
434497
),
435498
),
436499
)
@@ -453,6 +516,7 @@ export function createQueryHook({
453516
pageParam,
454517
nextPageParam,
455518
initialPageParam,
519+
nextPageParamType,
456520
),
457521
ts.factory.createSpreadAssignment(
458522
ts.factory.createIdentifier("options"),
@@ -637,6 +701,7 @@ function createInfiniteQueryParams(
637701
pageParam?: string,
638702
nextPageParam?: string,
639703
initialPageParam = "1",
704+
type?: string,
640705
) {
641706
if (pageParam === undefined || nextPageParam === undefined) {
642707
return [];
@@ -667,18 +732,23 @@ function createInfiniteQueryParams(
667732
ts.factory.createParenthesizedExpression(
668733
ts.factory.createAsExpression(
669734
ts.factory.createIdentifier("response"),
670-
nextPageParam.split(".").reduceRight((acc, segment) => {
671-
return ts.factory.createTypeLiteralNode([
672-
ts.factory.createPropertySignature(
673-
undefined,
674-
ts.factory.createIdentifier(segment),
675-
undefined,
676-
acc,
677-
),
678-
]);
679-
}, ts.factory.createKeywordTypeNode(
680-
ts.SyntaxKind.NumberKeyword,
681-
) as ts.TypeNode),
735+
nextPageParam.split(".").reduceRight(
736+
(acc, segment) => {
737+
return ts.factory.createTypeLiteralNode([
738+
ts.factory.createPropertySignature(
739+
undefined,
740+
ts.factory.createIdentifier(segment),
741+
undefined,
742+
acc,
743+
),
744+
]);
745+
},
746+
ts.factory.createKeywordTypeNode(
747+
type === "number"
748+
? ts.SyntaxKind.NumberKeyword
749+
: ts.SyntaxKind.StringKeyword,
750+
) as ts.TypeNode,
751+
),
682752
),
683753
),
684754
ts.factory.createIdentifier(nextPageParam),

0 commit comments

Comments
 (0)