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 #1420 from hey-api/fix/client-axios-query-styles

fix: add buildUrl and querySerializer to Axios client

authored by

Lubos and committed by
GitHub
f8274b40 9ad666d6

+596 -70
+5
.changeset/fast-laws-divide.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix: generate querySerializer options for Axios client
+5
.changeset/great-ears-fly.md
··· 1 + --- 2 + '@hey-api/client-axios': patch 3 + --- 4 + 5 + fix: add buildUrl method to Axios client API
+5
.changeset/smart-eyes-promise.md
··· 1 + --- 2 + '@hey-api/client-axios': minor 3 + --- 4 + 5 + feat: handle parameter styles the same way fetch client does if paramsSerializer is undefined
+5
.changeset/stale-swans-attend.md
··· 1 + --- 2 + '@hey-api/docs': patch 3 + --- 4 + 5 + docs: add buildUrl() method to Axios client page
+31
docs/openapi-ts/clients/axios.md
··· 140 140 }); 141 141 ``` 142 142 143 + ## Build URL 144 + 145 + ::: warning 146 + To use this feature, you must opt in to the [experimental parser](/openapi-ts/configuration#parser). 147 + ::: 148 + 149 + If you need to access the compiled URL, you can use the `buildUrl()` method. It's loosely typed by default to accept almost any value; in practice, you will want to pass a type hint. 150 + 151 + ```ts 152 + type FooData = { 153 + path: { 154 + fooId: number; 155 + }; 156 + query?: { 157 + bar?: string; 158 + }; 159 + url: '/foo/{fooId}'; 160 + }; 161 + 162 + const url = client.buildUrl<FooData>({ 163 + path: { 164 + fooId: 1, 165 + }, 166 + query: { 167 + bar: 'baz', 168 + }, 169 + url: '/foo/{fooId}', 170 + }); 171 + console.log(url); // prints '/foo/1?bar=baz' 172 + ``` 173 + 143 174 ## Bundling 144 175 145 176 Sometimes, you may not want to declare client packages as a dependency. This scenario is common if you're using Hey API to generate output that is repackaged and published for other consumers under your own brand. For such cases, our clients support bundling through the `client.bundle` configuration option.
+5 -6
packages/client-axios/src/index.ts
··· 3 3 4 4 import type { Client, Config } from './types'; 5 5 import { 6 + buildUrl, 6 7 createConfig, 7 - getUrl, 8 8 mergeConfigs, 9 9 mergeHeaders, 10 10 setAuthParams, ··· 48 48 opts.body = opts.bodySerializer(opts.body); 49 49 } 50 50 51 - const url = getUrl({ 52 - path: opts.path, 53 - url: opts.url, 54 - }); 51 + const url = buildUrl(opts); 55 52 56 53 try { 57 54 const response = await opts.axios({ 58 55 ...opts, 59 56 data: opts.body, 60 57 headers: opts.headers as RawAxiosRequestHeaders, 61 - params: opts.query, 58 + // let `paramsSerializer()` handle query params if it exists 59 + params: opts.paramsSerializer ? opts.query : undefined, 62 60 url, 63 61 }); 64 62 ··· 84 82 }; 85 83 86 84 return { 85 + buildUrl, 87 86 delete: (options) => request({ ...options, method: 'delete' }), 88 87 get: (options) => request({ ...options, method: 'get' }), 89 88 getConfig,
+29 -1
packages/client-axios/src/types.ts
··· 6 6 CreateAxiosDefaults, 7 7 } from 'axios'; 8 8 9 - import type { BodySerializer } from './utils'; 9 + import type { 10 + BodySerializer, 11 + QuerySerializer, 12 + QuerySerializerOptions, 13 + } from './utils'; 10 14 11 15 type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 12 16 ··· 68 72 | 'put' 69 73 | 'trace'; 70 74 /** 75 + * A function for serializing request query parameters. By default, arrays 76 + * will be exploded in form style, objects will be exploded in deepObject 77 + * style, and reserved characters are percent-encoded. 78 + * 79 + * This method will have no effect if the native `paramsSerializer()` Axios 80 + * API function is used. 81 + * 82 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 83 + */ 84 + querySerializer?: QuerySerializer | QuerySerializerOptions; 85 + /** 71 86 * A function for transforming response data before it's returned to the 72 87 * caller function. This is an ideal place to post-process server data, 73 88 * e.g. convert date ISO strings into native Date objects. ··· 141 156 ) => RequestResult<Data, TError, ThrowOnError>; 142 157 143 158 export interface Client { 159 + /** 160 + * Returns the final request URL. This method works only with experimental parser. 161 + */ 162 + buildUrl: < 163 + Data extends { 164 + body?: unknown; 165 + path?: Record<string, unknown>; 166 + query?: Record<string, unknown>; 167 + url: string; 168 + }, 169 + >( 170 + options: Pick<Data, 'url'> & Omit<Options<Data>, 'axios'>, 171 + ) => string; 144 172 delete: MethodFn; 145 173 get: MethodFn; 146 174 getConfig: () => Config;
+103 -3
packages/client-axios/src/utils.ts
··· 1 - import type { Config, RequestOptions, Security } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 12 12 type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 13 13 type ObjectStyle = 'form' | 'deepObject'; 14 14 type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 15 + 16 + export type QuerySerializer = (query: Record<string, unknown>) => string; 15 17 16 18 export type BodySerializer = (body: any) => any; 17 19 ··· 32 34 } 33 35 interface SerializePrimitiveParam extends SerializePrimitiveOptions { 34 36 value: string; 37 + } 38 + 39 + export interface QuerySerializerOptions { 40 + allowReserved?: boolean; 41 + array?: SerializerOptions<ArrayStyle>; 42 + object?: SerializerOptions<ObjectStyle>; 35 43 } 36 44 37 45 const serializePrimitiveParam = ({ ··· 250 258 return url; 251 259 }; 252 260 261 + export const createQuerySerializer = <T = unknown>({ 262 + allowReserved, 263 + array, 264 + object, 265 + }: QuerySerializerOptions = {}) => { 266 + const querySerializer = (queryParams: T) => { 267 + let search: string[] = []; 268 + if (queryParams && typeof queryParams === 'object') { 269 + for (const name in queryParams) { 270 + const value = queryParams[name]; 271 + 272 + if (value === undefined || value === null) { 273 + continue; 274 + } 275 + 276 + if (Array.isArray(value)) { 277 + search = [ 278 + ...search, 279 + serializeArrayParam({ 280 + allowReserved, 281 + explode: true, 282 + name, 283 + style: 'form', 284 + value, 285 + ...array, 286 + }), 287 + ]; 288 + continue; 289 + } 290 + 291 + if (typeof value === 'object') { 292 + search = [ 293 + ...search, 294 + serializeObjectParam({ 295 + allowReserved, 296 + explode: true, 297 + name, 298 + style: 'deepObject', 299 + value: value as Record<string, unknown>, 300 + ...object, 301 + }), 302 + ]; 303 + continue; 304 + } 305 + 306 + search = [ 307 + ...search, 308 + serializePrimitiveParam({ 309 + allowReserved, 310 + name, 311 + value: value as string, 312 + }), 313 + ]; 314 + } 315 + } 316 + return search.join('&'); 317 + }; 318 + return querySerializer; 319 + }; 320 + 253 321 export const getAuthToken = async ( 254 322 security: Security, 255 323 options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, ··· 297 365 } 298 366 }; 299 367 368 + export const buildUrl: Client['buildUrl'] = (options) => { 369 + const url = getUrl({ 370 + path: options.path, 371 + // let `paramsSerializer()` handle query params if it exists 372 + query: !options.paramsSerializer ? options.query : undefined, 373 + querySerializer: 374 + typeof options.querySerializer === 'function' 375 + ? options.querySerializer 376 + : createQuerySerializer(options.querySerializer), 377 + url: options.url, 378 + }); 379 + return url; 380 + }; 381 + 300 382 export const getUrl = ({ 301 383 path, 302 - url, 384 + query, 385 + querySerializer, 386 + url: _url, 303 387 }: { 304 388 path?: Record<string, unknown>; 389 + query?: Record<string, unknown>; 390 + querySerializer: QuerySerializer; 305 391 url: string; 306 - }) => (path ? defaultPathSerializer({ path, url }) : url); 392 + }) => { 393 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 394 + let url = pathUrl; 395 + if (path) { 396 + url = defaultPathSerializer({ path, url }); 397 + } 398 + let search = query ? querySerializer(query) : ''; 399 + if (search.startsWith('?')) { 400 + search = search.substring(1); 401 + } 402 + if (search) { 403 + url += `?${search}`; 404 + } 405 + return url; 406 + }; 307 407 308 408 const serializeFormDataPair = ( 309 409 formData: FormData,
+33 -35
packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
··· 292 292 } 293 293 } 294 294 295 - requestOptions.push({ 296 - key: 'url', 297 - value: operation.path, 298 - }); 295 + for (const name in operation.parameters?.query) { 296 + const parameter = operation.parameters.query[name]; 297 + if ( 298 + (parameter.schema.type === 'array' || 299 + parameter.schema.type === 'tuple') && 300 + (parameter.style !== 'form' || !parameter.explode) 301 + ) { 302 + // override the default settings for `querySerializer` 303 + requestOptions.push({ 304 + key: 'querySerializer', 305 + value: [ 306 + { 307 + key: 'array', 308 + value: [ 309 + { 310 + key: 'explode', 311 + value: false, 312 + }, 313 + { 314 + key: 'style', 315 + value: 'form', 316 + }, 317 + ], 318 + }, 319 + ], 320 + }); 321 + break; 322 + } 323 + } 299 324 300 325 const fileTransformers = context.file({ id: 'transformers' }); 301 326 if (fileTransformers) { ··· 315 340 } 316 341 } 317 342 318 - for (const name in operation.parameters?.query) { 319 - const parameter = operation.parameters.query[name]; 320 - if ( 321 - (parameter.schema.type === 'array' || 322 - parameter.schema.type === 'tuple') && 323 - (parameter.style !== 'form' || !parameter.explode) 324 - ) { 325 - // override the default settings for `querySerializer` 326 - if (context.config.client.name === '@hey-api/client-fetch') { 327 - requestOptions.push({ 328 - key: 'querySerializer', 329 - value: [ 330 - { 331 - key: 'array', 332 - value: [ 333 - { 334 - key: 'explode', 335 - value: false, 336 - }, 337 - { 338 - key: 'style', 339 - value: 'form', 340 - }, 341 - ], 342 - }, 343 - ], 344 - }); 345 - } 346 - break; 347 - } 348 - } 343 + requestOptions.push({ 344 + key: 'url', 345 + value: operation.path, 346 + }); 349 347 350 348 return [ 351 349 compiler.returnFunctionCall({
+9
packages/openapi-ts/test/3.0.x.test.ts
··· 402 402 }, 403 403 { 404 404 config: createConfig({ 405 + client: '@hey-api/client-axios', 406 + input: 'parameter-explode-false.json', 407 + output: 'parameter-explode-false-axios', 408 + plugins: ['@hey-api/sdk'], 409 + }), 410 + description: 'handles non-exploded array query parameters (Axios)', 411 + }, 412 + { 413 + config: createConfig({ 405 414 input: 'security-api-key.json', 406 415 output: 'security-api-key', 407 416 plugins: [
+9
packages/openapi-ts/test/3.1.x.test.ts
··· 440 440 }, 441 441 { 442 442 config: createConfig({ 443 + client: '@hey-api/client-axios', 444 + input: 'parameter-explode-false.json', 445 + output: 'parameter-explode-false-axios', 446 + plugins: ['@hey-api/sdk'], 447 + }), 448 + description: 'handles non-exploded array query parameters (Axios)', 449 + }, 450 + { 451 + config: createConfig({ 443 452 input: 'required-all-of-ref.json', 444 453 output: 'required-all-of-ref', 445 454 }),
+3
packages/openapi-ts/test/__snapshots__/3.0.x/parameter-explode-false-axios/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+19
packages/openapi-ts/test/__snapshots__/3.0.x/parameter-explode-false-axios/sdk.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createClient, createConfig, type Options } from '@hey-api/client-axios'; 4 + import type { PostFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const postFoo = <ThrowOnError extends boolean = false>(options?: Options<PostFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + querySerializer: { 12 + array: { 13 + explode: false, 14 + style: 'form' 15 + } 16 + }, 17 + url: '/foo' 18 + }); 19 + };
+17
packages/openapi-ts/test/__snapshots__/3.0.x/parameter-explode-false-axios/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type PostFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: { 7 + foo?: Array<string>; 8 + }; 9 + url: '/foo'; 10 + }; 11 + 12 + export type PostFooResponses = { 13 + /** 14 + * OK 15 + */ 16 + default: unknown; 17 + };
+2 -2
packages/openapi-ts/test/__snapshots__/3.0.x/parameter-explode-false/sdk.gen.ts
··· 8 8 export const postFoo = <ThrowOnError extends boolean = false>(options?: Options<PostFooData, ThrowOnError>) => { 9 9 return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({ 10 10 ...options, 11 - url: '/foo', 12 11 querySerializer: { 13 12 array: { 14 13 explode: false, 15 14 style: 'form' 16 15 } 17 - } 16 + }, 17 + url: '/foo' 18 18 }); 19 19 };
+3
packages/openapi-ts/test/__snapshots__/3.1.x/parameter-explode-false-axios/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+19
packages/openapi-ts/test/__snapshots__/3.1.x/parameter-explode-false-axios/sdk.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createClient, createConfig, type Options } from '@hey-api/client-axios'; 4 + import type { PostFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const postFoo = <ThrowOnError extends boolean = false>(options?: Options<PostFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + querySerializer: { 12 + array: { 13 + explode: false, 14 + style: 'form' 15 + } 16 + }, 17 + url: '/foo' 18 + }); 19 + };
+17
packages/openapi-ts/test/__snapshots__/3.1.x/parameter-explode-false-axios/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type PostFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: { 7 + foo?: Array<string>; 8 + }; 9 + url: '/foo'; 10 + }; 11 + 12 + export type PostFooResponses = { 13 + /** 14 + * OK 15 + */ 16 + default: unknown; 17 + };
+2 -2
packages/openapi-ts/test/__snapshots__/3.1.x/parameter-explode-false/sdk.gen.ts
··· 8 8 export const postFoo = <ThrowOnError extends boolean = false>(options?: Options<PostFooData, ThrowOnError>) => { 9 9 return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({ 10 10 ...options, 11 - url: '/foo', 12 11 querySerializer: { 13 12 array: { 14 13 explode: false, 15 14 style: 'form' 16 15 } 17 - } 16 + }, 17 + url: '/foo' 18 18 }); 19 19 };
+5 -6
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap
··· 3 3 4 4 import type { Client, Config } from './types'; 5 5 import { 6 + buildUrl, 6 7 createConfig, 7 - getUrl, 8 8 mergeConfigs, 9 9 mergeHeaders, 10 10 setAuthParams, ··· 48 48 opts.body = opts.bodySerializer(opts.body); 49 49 } 50 50 51 - const url = getUrl({ 52 - path: opts.path, 53 - url: opts.url, 54 - }); 51 + const url = buildUrl(opts); 55 52 56 53 try { 57 54 const response = await opts.axios({ 58 55 ...opts, 59 56 data: opts.body, 60 57 headers: opts.headers as RawAxiosRequestHeaders, 61 - params: opts.query, 58 + // let `paramsSerializer()` handle query params if it exists 59 + params: opts.paramsSerializer ? opts.query : undefined, 62 60 url, 63 61 }); 64 62 ··· 84 82 }; 85 83 86 84 return { 85 + buildUrl, 87 86 delete: (options) => request({ ...options, method: 'delete' }), 88 87 get: (options) => request({ ...options, method: 'get' }), 89 88 getConfig,
+29 -1
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap
··· 6 6 CreateAxiosDefaults, 7 7 } from 'axios'; 8 8 9 - import type { BodySerializer } from './utils'; 9 + import type { 10 + BodySerializer, 11 + QuerySerializer, 12 + QuerySerializerOptions, 13 + } from './utils'; 10 14 11 15 type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 12 16 ··· 68 72 | 'put' 69 73 | 'trace'; 70 74 /** 75 + * A function for serializing request query parameters. By default, arrays 76 + * will be exploded in form style, objects will be exploded in deepObject 77 + * style, and reserved characters are percent-encoded. 78 + * 79 + * This method will have no effect if the native `paramsSerializer()` Axios 80 + * API function is used. 81 + * 82 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 83 + */ 84 + querySerializer?: QuerySerializer | QuerySerializerOptions; 85 + /** 71 86 * A function for transforming response data before it's returned to the 72 87 * caller function. This is an ideal place to post-process server data, 73 88 * e.g. convert date ISO strings into native Date objects. ··· 141 156 ) => RequestResult<Data, TError, ThrowOnError>; 142 157 143 158 export interface Client { 159 + /** 160 + * Returns the final request URL. This method works only with experimental parser. 161 + */ 162 + buildUrl: < 163 + Data extends { 164 + body?: unknown; 165 + path?: Record<string, unknown>; 166 + query?: Record<string, unknown>; 167 + url: string; 168 + }, 169 + >( 170 + options: Pick<Data, 'url'> & Omit<Options<Data>, 'axios'>, 171 + ) => string; 144 172 delete: MethodFn; 145 173 get: MethodFn; 146 174 getConfig: () => Config;
+103 -3
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/utils.ts.snap
··· 1 - import type { Config, RequestOptions, Security } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 12 12 type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 13 13 type ObjectStyle = 'form' | 'deepObject'; 14 14 type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 15 + 16 + export type QuerySerializer = (query: Record<string, unknown>) => string; 15 17 16 18 export type BodySerializer = (body: any) => any; 17 19 ··· 32 34 } 33 35 interface SerializePrimitiveParam extends SerializePrimitiveOptions { 34 36 value: string; 37 + } 38 + 39 + export interface QuerySerializerOptions { 40 + allowReserved?: boolean; 41 + array?: SerializerOptions<ArrayStyle>; 42 + object?: SerializerOptions<ObjectStyle>; 35 43 } 36 44 37 45 const serializePrimitiveParam = ({ ··· 250 258 return url; 251 259 }; 252 260 261 + export const createQuerySerializer = <T = unknown>({ 262 + allowReserved, 263 + array, 264 + object, 265 + }: QuerySerializerOptions = {}) => { 266 + const querySerializer = (queryParams: T) => { 267 + let search: string[] = []; 268 + if (queryParams && typeof queryParams === 'object') { 269 + for (const name in queryParams) { 270 + const value = queryParams[name]; 271 + 272 + if (value === undefined || value === null) { 273 + continue; 274 + } 275 + 276 + if (Array.isArray(value)) { 277 + search = [ 278 + ...search, 279 + serializeArrayParam({ 280 + allowReserved, 281 + explode: true, 282 + name, 283 + style: 'form', 284 + value, 285 + ...array, 286 + }), 287 + ]; 288 + continue; 289 + } 290 + 291 + if (typeof value === 'object') { 292 + search = [ 293 + ...search, 294 + serializeObjectParam({ 295 + allowReserved, 296 + explode: true, 297 + name, 298 + style: 'deepObject', 299 + value: value as Record<string, unknown>, 300 + ...object, 301 + }), 302 + ]; 303 + continue; 304 + } 305 + 306 + search = [ 307 + ...search, 308 + serializePrimitiveParam({ 309 + allowReserved, 310 + name, 311 + value: value as string, 312 + }), 313 + ]; 314 + } 315 + } 316 + return search.join('&'); 317 + }; 318 + return querySerializer; 319 + }; 320 + 253 321 export const getAuthToken = async ( 254 322 security: Security, 255 323 options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, ··· 297 365 } 298 366 }; 299 367 368 + export const buildUrl: Client['buildUrl'] = (options) => { 369 + const url = getUrl({ 370 + path: options.path, 371 + // let `paramsSerializer()` handle query params if it exists 372 + query: !options.paramsSerializer ? options.query : undefined, 373 + querySerializer: 374 + typeof options.querySerializer === 'function' 375 + ? options.querySerializer 376 + : createQuerySerializer(options.querySerializer), 377 + url: options.url, 378 + }); 379 + return url; 380 + }; 381 + 300 382 export const getUrl = ({ 301 383 path, 302 - url, 384 + query, 385 + querySerializer, 386 + url: _url, 303 387 }: { 304 388 path?: Record<string, unknown>; 389 + query?: Record<string, unknown>; 390 + querySerializer: QuerySerializer; 305 391 url: string; 306 - }) => (path ? defaultPathSerializer({ path, url }) : url); 392 + }) => { 393 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 394 + let url = pathUrl; 395 + if (path) { 396 + url = defaultPathSerializer({ path, url }); 397 + } 398 + let search = query ? querySerializer(query) : ''; 399 + if (search.startsWith('?')) { 400 + search = search.substring(1); 401 + } 402 + if (search) { 403 + url += `?${search}`; 404 + } 405 + return url; 406 + }; 307 407 308 408 const serializeFormDataPair = ( 309 409 formData: FormData,
+5 -6
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap
··· 3 3 4 4 import type { Client, Config } from './types'; 5 5 import { 6 + buildUrl, 6 7 createConfig, 7 - getUrl, 8 8 mergeConfigs, 9 9 mergeHeaders, 10 10 setAuthParams, ··· 48 48 opts.body = opts.bodySerializer(opts.body); 49 49 } 50 50 51 - const url = getUrl({ 52 - path: opts.path, 53 - url: opts.url, 54 - }); 51 + const url = buildUrl(opts); 55 52 56 53 try { 57 54 const response = await opts.axios({ 58 55 ...opts, 59 56 data: opts.body, 60 57 headers: opts.headers as RawAxiosRequestHeaders, 61 - params: opts.query, 58 + // let `paramsSerializer()` handle query params if it exists 59 + params: opts.paramsSerializer ? opts.query : undefined, 62 60 url, 63 61 }); 64 62 ··· 84 82 }; 85 83 86 84 return { 85 + buildUrl, 87 86 delete: (options) => request({ ...options, method: 'delete' }), 88 87 get: (options) => request({ ...options, method: 'get' }), 89 88 getConfig,
+29 -1
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap
··· 6 6 CreateAxiosDefaults, 7 7 } from 'axios'; 8 8 9 - import type { BodySerializer } from './utils'; 9 + import type { 10 + BodySerializer, 11 + QuerySerializer, 12 + QuerySerializerOptions, 13 + } from './utils'; 10 14 11 15 type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 12 16 ··· 68 72 | 'put' 69 73 | 'trace'; 70 74 /** 75 + * A function for serializing request query parameters. By default, arrays 76 + * will be exploded in form style, objects will be exploded in deepObject 77 + * style, and reserved characters are percent-encoded. 78 + * 79 + * This method will have no effect if the native `paramsSerializer()` Axios 80 + * API function is used. 81 + * 82 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 83 + */ 84 + querySerializer?: QuerySerializer | QuerySerializerOptions; 85 + /** 71 86 * A function for transforming response data before it's returned to the 72 87 * caller function. This is an ideal place to post-process server data, 73 88 * e.g. convert date ISO strings into native Date objects. ··· 141 156 ) => RequestResult<Data, TError, ThrowOnError>; 142 157 143 158 export interface Client { 159 + /** 160 + * Returns the final request URL. This method works only with experimental parser. 161 + */ 162 + buildUrl: < 163 + Data extends { 164 + body?: unknown; 165 + path?: Record<string, unknown>; 166 + query?: Record<string, unknown>; 167 + url: string; 168 + }, 169 + >( 170 + options: Pick<Data, 'url'> & Omit<Options<Data>, 'axios'>, 171 + ) => string; 144 172 delete: MethodFn; 145 173 get: MethodFn; 146 174 getConfig: () => Config;
+103 -3
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/utils.ts.snap
··· 1 - import type { Config, RequestOptions, Security } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 12 12 type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 13 13 type ObjectStyle = 'form' | 'deepObject'; 14 14 type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 15 + 16 + export type QuerySerializer = (query: Record<string, unknown>) => string; 15 17 16 18 export type BodySerializer = (body: any) => any; 17 19 ··· 32 34 } 33 35 interface SerializePrimitiveParam extends SerializePrimitiveOptions { 34 36 value: string; 37 + } 38 + 39 + export interface QuerySerializerOptions { 40 + allowReserved?: boolean; 41 + array?: SerializerOptions<ArrayStyle>; 42 + object?: SerializerOptions<ObjectStyle>; 35 43 } 36 44 37 45 const serializePrimitiveParam = ({ ··· 250 258 return url; 251 259 }; 252 260 261 + export const createQuerySerializer = <T = unknown>({ 262 + allowReserved, 263 + array, 264 + object, 265 + }: QuerySerializerOptions = {}) => { 266 + const querySerializer = (queryParams: T) => { 267 + let search: string[] = []; 268 + if (queryParams && typeof queryParams === 'object') { 269 + for (const name in queryParams) { 270 + const value = queryParams[name]; 271 + 272 + if (value === undefined || value === null) { 273 + continue; 274 + } 275 + 276 + if (Array.isArray(value)) { 277 + search = [ 278 + ...search, 279 + serializeArrayParam({ 280 + allowReserved, 281 + explode: true, 282 + name, 283 + style: 'form', 284 + value, 285 + ...array, 286 + }), 287 + ]; 288 + continue; 289 + } 290 + 291 + if (typeof value === 'object') { 292 + search = [ 293 + ...search, 294 + serializeObjectParam({ 295 + allowReserved, 296 + explode: true, 297 + name, 298 + style: 'deepObject', 299 + value: value as Record<string, unknown>, 300 + ...object, 301 + }), 302 + ]; 303 + continue; 304 + } 305 + 306 + search = [ 307 + ...search, 308 + serializePrimitiveParam({ 309 + allowReserved, 310 + name, 311 + value: value as string, 312 + }), 313 + ]; 314 + } 315 + } 316 + return search.join('&'); 317 + }; 318 + return querySerializer; 319 + }; 320 + 253 321 export const getAuthToken = async ( 254 322 security: Security, 255 323 options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, ··· 297 365 } 298 366 }; 299 367 368 + export const buildUrl: Client['buildUrl'] = (options) => { 369 + const url = getUrl({ 370 + path: options.path, 371 + // let `paramsSerializer()` handle query params if it exists 372 + query: !options.paramsSerializer ? options.query : undefined, 373 + querySerializer: 374 + typeof options.querySerializer === 'function' 375 + ? options.querySerializer 376 + : createQuerySerializer(options.querySerializer), 377 + url: options.url, 378 + }); 379 + return url; 380 + }; 381 + 300 382 export const getUrl = ({ 301 383 path, 302 - url, 384 + query, 385 + querySerializer, 386 + url: _url, 303 387 }: { 304 388 path?: Record<string, unknown>; 389 + query?: Record<string, unknown>; 390 + querySerializer: QuerySerializer; 305 391 url: string; 306 - }) => (path ? defaultPathSerializer({ path, url }) : url); 392 + }) => { 393 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 394 + let url = pathUrl; 395 + if (path) { 396 + url = defaultPathSerializer({ path, url }); 397 + } 398 + let search = query ? querySerializer(query) : ''; 399 + if (search.startsWith('?')) { 400 + search = search.substring(1); 401 + } 402 + if (search) { 403 + url += `?${search}`; 404 + } 405 + return url; 406 + }; 307 407 308 408 const serializeFormDataPair = ( 309 409 formData: FormData,
+1 -1
packages/openapi-ts/test/sample.cjs
··· 13 13 exclude: '^#/components/schemas/ModelWithCircularReference$', 14 14 // include: 15 15 // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', 16 - path: './test/spec/3.1.x/schema-recursive.json', 16 + path: './test/spec/3.1.x/parameter-explode-false.json', 17 17 // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 18 18 // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 19 19 },