Skip to content

Commit f6e02d7

Browse files
authored
Merge pull request #1913 from BogdanMaier/fix/code-genertion-when-optional-field-pagination-is-missing
fix(pagination-parsing): code generation when optional pagination field is missing
2 parents 3f39137 + 0806a2e commit f6e02d7

File tree

3 files changed

+216
-19
lines changed

3 files changed

+216
-19
lines changed

.changeset/giant-jeans-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
fix: prevent crash when optional pagination field is missing

packages/openapi-ts/src/ir/__tests__/pagination.test.ts

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it, vi } from 'vitest';
22

33
import type { Config } from '../../types/config';
4+
import { operationPagination } from '../operation';
45
import { getPaginationKeywordsRegExp } from '../pagination';
6+
import type { IR } from '../types';
57

68
describe('paginationKeywordsRegExp', () => {
79
const defaultScenarios: Array<{
@@ -66,9 +68,190 @@ describe('paginationKeywordsRegExp', () => {
6668
const pagination: Config['input']['pagination'] = {
6769
keywords: ['customPagination', 'pageSize', 'perPage'],
6870
};
69-
7071
const paginationRegExp = getPaginationKeywordsRegExp(pagination);
7172
expect(paginationRegExp.test(value)).toEqual(result);
7273
},
7374
);
7475
});
76+
77+
describe('operationPagination', () => {
78+
const queryParam = (
79+
name: string,
80+
type: IR.SchemaObject['type'] = 'string',
81+
pagination = false,
82+
): IR.ParameterObject => ({
83+
explode: true,
84+
location: 'query',
85+
name,
86+
schema: { type },
87+
style: 'form',
88+
...(pagination ? { pagination: true } : {}),
89+
});
90+
91+
const emptyContext = {} as IR.Context;
92+
93+
const baseOperationMeta = {
94+
method: 'post' as const,
95+
path: '/test' as const,
96+
};
97+
98+
const queryScenarios: Array<{
99+
hasPagination: boolean;
100+
operation: IR.OperationObject;
101+
}> = [
102+
{
103+
hasPagination: true,
104+
operation: {
105+
...baseOperationMeta,
106+
id: 'op1',
107+
method: 'get',
108+
parameters: {
109+
query: {
110+
page: queryParam('page', 'integer', true),
111+
},
112+
},
113+
},
114+
},
115+
{
116+
hasPagination: false,
117+
operation: {
118+
...baseOperationMeta,
119+
id: 'op2',
120+
method: 'get',
121+
parameters: {
122+
query: {
123+
sort: queryParam('sort', 'string'),
124+
},
125+
},
126+
},
127+
},
128+
];
129+
130+
it.each(queryScenarios)(
131+
'query params for $operation.id → $hasPagination',
132+
({
133+
hasPagination,
134+
operation,
135+
}: {
136+
hasPagination: boolean;
137+
operation: IR.OperationObject;
138+
}) => {
139+
const result = operationPagination({ context: emptyContext, operation });
140+
expect(Boolean(result)).toEqual(hasPagination);
141+
},
142+
);
143+
144+
it('body.pagination === true returns entire body', () => {
145+
const operation: IR.OperationObject = {
146+
...baseOperationMeta,
147+
body: {
148+
mediaType: 'application/json',
149+
pagination: true,
150+
schema: {
151+
properties: {
152+
page: { type: 'integer' },
153+
},
154+
type: 'object',
155+
},
156+
},
157+
id: 'bodyTrue',
158+
};
159+
160+
const result = operationPagination({ context: emptyContext, operation });
161+
162+
expect(result?.in).toEqual('body');
163+
expect(result?.name).toEqual('body');
164+
expect(result?.schema?.type).toEqual('object');
165+
});
166+
167+
it('body.pagination = "pagination" returns the matching property', () => {
168+
const operation: IR.OperationObject = {
169+
...baseOperationMeta,
170+
body: {
171+
mediaType: 'application/json',
172+
pagination: 'pagination',
173+
schema: {
174+
properties: {
175+
pagination: {
176+
properties: {
177+
page: { type: 'integer' },
178+
},
179+
type: 'object',
180+
},
181+
},
182+
type: 'object',
183+
},
184+
},
185+
id: 'bodyField',
186+
};
187+
188+
const result = operationPagination({ context: emptyContext, operation });
189+
190+
expect(result?.in).toEqual('body');
191+
expect(result?.name).toEqual('pagination');
192+
expect(result?.schema?.type).toEqual('object');
193+
});
194+
195+
it('resolves $ref and uses the resolved pagination property', () => {
196+
const context: IR.Context = {
197+
resolveIrRef: vi.fn().mockReturnValue({
198+
properties: {
199+
pagination: {
200+
properties: {
201+
page: { type: 'integer' },
202+
},
203+
type: 'object',
204+
},
205+
},
206+
type: 'object',
207+
}),
208+
} as unknown as IR.Context;
209+
210+
const operation: IR.OperationObject = {
211+
...baseOperationMeta,
212+
body: {
213+
mediaType: 'application/json',
214+
pagination: 'pagination',
215+
schema: { $ref: '#/components/schemas/PaginationBody' },
216+
},
217+
id: 'refPagination',
218+
};
219+
220+
const result = operationPagination({ context, operation });
221+
222+
expect(context.resolveIrRef).toHaveBeenCalledWith(
223+
'#/components/schemas/PaginationBody',
224+
);
225+
expect(result?.in).toEqual('body');
226+
expect(result?.name).toEqual('pagination');
227+
expect(result?.schema?.type).toEqual('object');
228+
});
229+
230+
it('falls back to query when pagination key not found in body', () => {
231+
const operation: IR.OperationObject = {
232+
...baseOperationMeta,
233+
body: {
234+
mediaType: 'application/json',
235+
pagination: 'pagination',
236+
schema: {
237+
properties: {
238+
notPagination: { type: 'string' },
239+
},
240+
type: 'object',
241+
},
242+
},
243+
id: 'fallback',
244+
parameters: {
245+
query: {
246+
cursor: queryParam('cursor', 'string', true),
247+
},
248+
},
249+
};
250+
251+
const result = operationPagination({ context: emptyContext, operation });
252+
253+
expect(result?.in).toEqual('query');
254+
expect(result?.name).toEqual('cursor');
255+
expect(result?.schema?.type).toEqual('string');
256+
});
257+
});

packages/openapi-ts/src/ir/operation.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,38 @@ export const operationPagination = ({
2828
context: IR.Context;
2929
operation: IR.OperationObject;
3030
}): Pagination | undefined => {
31-
if (operation.body?.pagination) {
32-
if (typeof operation.body.pagination === 'boolean') {
33-
return {
34-
in: 'body',
35-
name: 'body',
36-
schema: operation.body.schema,
37-
};
38-
}
31+
const body = operation.body;
32+
33+
if (!body || !body.pagination) {
34+
return parameterWithPagination(operation.parameters);
35+
}
3936

40-
const schema = operation.body.schema.$ref
41-
? context.resolveIrRef<IR.RequestBodyObject | IR.SchemaObject>(
42-
operation.body.schema.$ref,
43-
)
44-
: operation.body.schema;
45-
const finalSchema = 'schema' in schema ? schema.schema : schema;
37+
if (body.pagination === true) {
4638
return {
4739
in: 'body',
48-
name: operation.body.pagination,
49-
schema: finalSchema.properties![operation.body.pagination]!,
40+
name: 'body',
41+
schema: body.schema,
5042
};
5143
}
5244

53-
return parameterWithPagination(operation.parameters);
45+
const schema = body.schema;
46+
const resolvedSchema = schema.$ref
47+
? context.resolveIrRef<IR.RequestBodyObject | IR.SchemaObject>(schema.$ref)
48+
: schema;
49+
50+
const finalSchema =
51+
'schema' in resolvedSchema ? resolvedSchema.schema : resolvedSchema;
52+
const paginationProp = finalSchema?.properties?.[body.pagination];
53+
54+
if (!paginationProp) {
55+
return parameterWithPagination(operation.parameters);
56+
}
57+
58+
return {
59+
in: 'body',
60+
name: body.pagination,
61+
schema: paginationProp,
62+
};
5463
};
5564

5665
type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default';

0 commit comments

Comments
 (0)