fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Lubos and committed by
GitHub
f6e02d76 3f391375

+216 -19
+5
.changeset/giant-jeans-study.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + fix: prevent crash when optional pagination field is missing
+185 -2
packages/openapi-ts/src/ir/__tests__/pagination.test.ts
··· 1 - import { describe, expect, it } from 'vitest'; 1 + import { describe, expect, it, vi } from 'vitest'; 2 2 3 3 import type { Config } from '../../types/config'; 4 + import { operationPagination } from '../operation'; 4 5 import { getPaginationKeywordsRegExp } from '../pagination'; 6 + import type { IR } from '../types'; 5 7 6 8 describe('paginationKeywordsRegExp', () => { 7 9 const defaultScenarios: Array<{ ··· 66 68 const pagination: Config['input']['pagination'] = { 67 69 keywords: ['customPagination', 'pageSize', 'perPage'], 68 70 }; 69 - 70 71 const paginationRegExp = getPaginationKeywordsRegExp(pagination); 71 72 expect(paginationRegExp.test(value)).toEqual(result); 72 73 }, 73 74 ); 74 75 }); 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 + });
+26 -17
packages/openapi-ts/src/ir/operation.ts
··· 28 28 context: IR.Context; 29 29 operation: IR.OperationObject; 30 30 }): 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; 39 32 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; 33 + if (!body || !body.pagination) { 34 + return parameterWithPagination(operation.parameters); 35 + } 36 + 37 + if (body.pagination === true) { 46 38 return { 47 39 in: 'body', 48 - name: operation.body.pagination, 49 - schema: finalSchema.properties![operation.body.pagination]!, 40 + name: 'body', 41 + schema: body.schema, 50 42 }; 51 43 } 52 44 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 + }; 54 63 }; 55 64 56 65 type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default';