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 #1409 from hey-api/feat/security-basic

feat: support oauth2 and apiKey security schemes

authored by

Lubos and committed by
GitHub
a7c0e2a0 db99308f

+1589 -128
+7
.changeset/strong-countries-fail.md
··· 1 + --- 2 + '@hey-api/client-axios': patch 3 + '@hey-api/client-fetch': patch 4 + '@hey-api/openapi-ts': patch 5 + --- 6 + 7 + feat: support oauth2 and apiKey security schemes
+1
docs/openapi-ts/plugins.md
··· 67 67 name: 'my-plugin'; 68 68 /** 69 69 * Name of the generated file. 70 + * 70 71 * @default 'my-plugin' 71 72 */ 72 73 output?: string;
+175
packages/client-axios/src/__tests__/utils.test.ts
··· 1 + import { describe, expect, it, vi } from 'vitest'; 2 + 3 + import { getAuthToken, setAuthParams } from '../utils'; 4 + 5 + describe('getAuthToken', () => { 6 + it('returns access token', async () => { 7 + const accessToken = vi.fn().mockReturnValue('foo'); 8 + const apiKey = vi.fn().mockReturnValue('bar'); 9 + const token = await getAuthToken( 10 + { 11 + fn: 'accessToken', 12 + in: 'header', 13 + name: 'baz', 14 + }, 15 + { 16 + accessToken, 17 + apiKey, 18 + }, 19 + ); 20 + expect(accessToken).toHaveBeenCalled(); 21 + expect(token).toBe('Bearer foo'); 22 + }); 23 + 24 + it('returns nothing when accessToken function is undefined', async () => { 25 + const apiKey = vi.fn().mockReturnValue('bar'); 26 + const token = await getAuthToken( 27 + { 28 + fn: 'accessToken', 29 + in: 'header', 30 + name: 'baz', 31 + }, 32 + { 33 + apiKey, 34 + }, 35 + ); 36 + expect(token).toBeUndefined(); 37 + }); 38 + 39 + it('returns API key', async () => { 40 + const accessToken = vi.fn().mockReturnValue('foo'); 41 + const apiKey = vi.fn().mockReturnValue('bar'); 42 + const token = await getAuthToken( 43 + { 44 + fn: 'apiKey', 45 + in: 'header', 46 + name: 'baz', 47 + }, 48 + { 49 + accessToken, 50 + apiKey, 51 + }, 52 + ); 53 + expect(apiKey).toHaveBeenCalled(); 54 + expect(token).toBe('bar'); 55 + }); 56 + 57 + it('returns nothing when apiKey function is undefined', async () => { 58 + const accessToken = vi.fn().mockReturnValue('foo'); 59 + const token = await getAuthToken( 60 + { 61 + fn: 'apiKey', 62 + in: 'header', 63 + name: 'baz', 64 + }, 65 + { 66 + accessToken, 67 + }, 68 + ); 69 + expect(token).toBeUndefined(); 70 + }); 71 + }); 72 + 73 + describe('setAuthParams', () => { 74 + it('sets access token in headers', async () => { 75 + const accessToken = vi.fn().mockReturnValue('foo'); 76 + const apiKey = vi.fn().mockReturnValue('bar'); 77 + const headers: Record<any, unknown> = {}; 78 + const query: Record<any, unknown> = {}; 79 + await setAuthParams({ 80 + accessToken, 81 + apiKey, 82 + headers, 83 + query, 84 + security: [ 85 + { 86 + fn: 'accessToken', 87 + in: 'header', 88 + name: 'baz', 89 + }, 90 + ], 91 + }); 92 + expect(accessToken).toHaveBeenCalled(); 93 + expect(headers.baz).toBe('Bearer foo'); 94 + expect(Object.keys(query).length).toBe(0); 95 + }); 96 + 97 + it('sets access token in query', async () => { 98 + const accessToken = vi.fn().mockReturnValue('foo'); 99 + const apiKey = vi.fn().mockReturnValue('bar'); 100 + const headers: Record<any, unknown> = {}; 101 + const query: Record<any, unknown> = {}; 102 + await setAuthParams({ 103 + accessToken, 104 + apiKey, 105 + headers, 106 + query, 107 + security: [ 108 + { 109 + fn: 'accessToken', 110 + in: 'query', 111 + name: 'baz', 112 + }, 113 + ], 114 + }); 115 + expect(accessToken).toHaveBeenCalled(); 116 + expect(Object.keys(headers).length).toBe(0); 117 + expect(query.baz).toBe('Bearer foo'); 118 + }); 119 + 120 + it('sets first scheme only', async () => { 121 + const accessToken = vi.fn().mockReturnValue('foo'); 122 + const apiKey = vi.fn().mockReturnValue('bar'); 123 + const headers: Record<any, unknown> = {}; 124 + const query: Record<any, unknown> = {}; 125 + await setAuthParams({ 126 + accessToken, 127 + apiKey, 128 + headers, 129 + query, 130 + security: [ 131 + { 132 + fn: 'accessToken', 133 + in: 'header', 134 + name: 'baz', 135 + }, 136 + { 137 + fn: 'accessToken', 138 + in: 'query', 139 + name: 'baz', 140 + }, 141 + ], 142 + }); 143 + expect(accessToken).toHaveBeenCalled(); 144 + expect(headers.baz).toBe('Bearer foo'); 145 + expect(Object.keys(query).length).toBe(0); 146 + }); 147 + 148 + it('sets first scheme with token', async () => { 149 + const accessToken = vi.fn().mockReturnValue('foo'); 150 + const apiKey = vi.fn().mockReturnValue(undefined); 151 + const headers: Record<any, unknown> = {}; 152 + const query: Record<any, unknown> = {}; 153 + await setAuthParams({ 154 + accessToken, 155 + apiKey, 156 + headers, 157 + query, 158 + security: [ 159 + { 160 + fn: 'apiKey', 161 + in: 'header', 162 + name: 'baz', 163 + }, 164 + { 165 + fn: 'accessToken', 166 + in: 'query', 167 + name: 'baz', 168 + }, 169 + ], 170 + }); 171 + expect(accessToken).toHaveBeenCalled(); 172 + expect(Object.keys(headers).length).toBe(0); 173 + expect(query.baz).toBe('Bearer foo'); 174 + }); 175 + });
+19 -8
packages/client-axios/src/index.ts
··· 2 2 import axios from 'axios'; 3 3 4 4 import type { Client, Config } from './types'; 5 - import { createConfig, getUrl, mergeConfigs, mergeHeaders } from './utils'; 5 + import { 6 + createConfig, 7 + getUrl, 8 + mergeConfigs, 9 + mergeHeaders, 10 + setAuthParams, 11 + } from './utils'; 6 12 7 13 export const createClient = (config: Config): Client => { 8 14 let _config = mergeConfigs(createConfig(), config); ··· 27 33 const opts = { 28 34 ..._config, 29 35 ...options, 30 - headers: mergeHeaders( 31 - _config.headers, 32 - options.headers, 33 - ) as RawAxiosRequestHeaders, 36 + axios: options.axios ?? _config.axios ?? instance, 37 + headers: mergeHeaders(_config.headers, options.headers), 34 38 }; 39 + 40 + if (opts.security) { 41 + await setAuthParams({ 42 + ...opts, 43 + security: opts.security, 44 + }); 45 + } 46 + 35 47 if (opts.body && opts.bodySerializer) { 36 48 opts.body = opts.bodySerializer(opts.body); 37 49 } ··· 41 53 url: opts.url, 42 54 }); 43 55 44 - const _axios = opts.axios || instance; 45 - 46 56 try { 47 - const response = await _axios({ 57 + const response = await opts.axios({ 48 58 ...opts, 49 59 data: opts.body, 60 + headers: opts.headers as RawAxiosRequestHeaders, 50 61 params: opts.query, 51 62 url, 52 63 });
+22
packages/client-axios/src/types.ts
··· 13 13 export interface Config<ThrowOnError extends boolean = boolean> 14 14 extends Omit<CreateAxiosDefaults, 'headers'> { 15 15 /** 16 + * Access token or a function returning access token. The resolved token will 17 + * be added to request payload as required. 18 + */ 19 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 20 + /** 21 + * API key or a function returning API key. The resolved key will be added 22 + * to the request payload as required. 23 + */ 24 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 25 + /** 16 26 * Axios implementation. You can use this option to provide a custom 17 27 * Axios instance. 28 + * 18 29 * @default axios 19 30 */ 20 31 axios?: AxiosStatic; ··· 64 75 responseTransformer?: (data: unknown) => Promise<unknown>; 65 76 /** 66 77 * Throw an error instead of returning it in the response? 78 + * 67 79 * @default false 68 80 */ 69 81 throwOnError?: ThrowOnError; ··· 87 99 client?: Client; 88 100 path?: Record<string, unknown>; 89 101 query?: Record<string, unknown>; 102 + /** 103 + * Security mechanism(s) to use for the request. 104 + */ 105 + security?: ReadonlyArray<Security>; 90 106 url: Url; 91 107 } 92 108 ··· 100 116 | (AxiosResponse<Data> & { error: undefined }) 101 117 | (AxiosError<TError> & { data: undefined; error: TError }) 102 118 >; 119 + 120 + export interface Security { 121 + fn: 'accessToken' | 'apiKey'; 122 + in: 'header' | 'query'; 123 + name: string; 124 + } 103 125 104 126 type MethodFn = < 105 127 Data = unknown,
+50 -5
packages/client-axios/src/utils.ts
··· 1 - import type { Config } from './types'; 1 + import type { Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 250 250 return url; 251 251 }; 252 252 253 + export const getAuthToken = async ( 254 + security: Security, 255 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 256 + ): Promise<string | undefined> => { 257 + if (security.fn === 'accessToken') { 258 + const token = 259 + typeof options.accessToken === 'function' 260 + ? await options.accessToken() 261 + : options.accessToken; 262 + return token ? `Bearer ${token}` : undefined; 263 + } 264 + 265 + if (security.fn === 'apiKey') { 266 + return typeof options.apiKey === 'function' 267 + ? await options.apiKey() 268 + : options.apiKey; 269 + } 270 + }; 271 + 272 + export const setAuthParams = async ({ 273 + security, 274 + ...options 275 + }: Pick<Required<RequestOptions>, 'security'> & 276 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 277 + headers: Record<any, unknown>; 278 + }) => { 279 + for (const scheme of security) { 280 + const token = await getAuthToken(scheme, options); 281 + 282 + if (!token) { 283 + continue; 284 + } 285 + 286 + if (scheme.in === 'header') { 287 + options.headers[scheme.name] = token; 288 + } else if (scheme.in === 'query') { 289 + if (!options.query) { 290 + options.query = {}; 291 + } 292 + 293 + options.query[scheme.name] = token; 294 + } 295 + 296 + return; 297 + } 298 + }; 299 + 253 300 export const getUrl = ({ 254 301 path, 255 302 url, ··· 278 325 279 326 export const mergeHeaders = ( 280 327 ...headers: Array<Required<Config>['headers'] | undefined> 281 - ): Required<Config>['headers'] => { 282 - const mergedHeaders: Required<Config>['headers'] = {}; 328 + ): Record<any, unknown> => { 329 + const mergedHeaders: Record<any, unknown> = {}; 283 330 for (const header of headers) { 284 331 if (!header || typeof header !== 'object') { 285 332 continue; ··· 289 336 290 337 for (const [key, value] of iterator) { 291 338 if (value === null) { 292 - // @ts-expect-error 293 339 delete mergedHeaders[key]; 294 340 } else if (Array.isArray(value)) { 295 341 for (const v of value) { ··· 299 345 } else if (value !== undefined) { 300 346 // assume object headers are meant to be JSON stringified, i.e. their 301 347 // content value in OpenAPI specification is 'application/json' 302 - // @ts-expect-error 303 348 mergedHeaders[key] = 304 349 typeof value === 'object' ? JSON.stringify(value) : (value as string); 305 350 }
-7
packages/client-axios/test/index.test.ts
··· 1 - import { describe, expect, it } from 'vitest'; 2 - 3 - describe('Axios client', () => { 4 - it('works', () => { 5 - expect(1).toBe(1); 6 - }); 7 - });
+238
packages/client-fetch/src/__tests__/utils.test.ts
··· 1 + import { describe, expect, it, vi } from 'vitest'; 2 + 3 + import { getAuthToken, getParseAs, setAuthParams } from '../utils'; 4 + 5 + describe('getAuthToken', () => { 6 + it('returns access token', async () => { 7 + const accessToken = vi.fn().mockReturnValue('foo'); 8 + const apiKey = vi.fn().mockReturnValue('bar'); 9 + const token = await getAuthToken( 10 + { 11 + fn: 'accessToken', 12 + in: 'header', 13 + name: 'baz', 14 + }, 15 + { 16 + accessToken, 17 + apiKey, 18 + }, 19 + ); 20 + expect(accessToken).toHaveBeenCalled(); 21 + expect(token).toBe('Bearer foo'); 22 + }); 23 + 24 + it('returns nothing when accessToken function is undefined', async () => { 25 + const apiKey = vi.fn().mockReturnValue('bar'); 26 + const token = await getAuthToken( 27 + { 28 + fn: 'accessToken', 29 + in: 'header', 30 + name: 'baz', 31 + }, 32 + { 33 + apiKey, 34 + }, 35 + ); 36 + expect(token).toBeUndefined(); 37 + }); 38 + 39 + it('returns API key', async () => { 40 + const accessToken = vi.fn().mockReturnValue('foo'); 41 + const apiKey = vi.fn().mockReturnValue('bar'); 42 + const token = await getAuthToken( 43 + { 44 + fn: 'apiKey', 45 + in: 'header', 46 + name: 'baz', 47 + }, 48 + { 49 + accessToken, 50 + apiKey, 51 + }, 52 + ); 53 + expect(apiKey).toHaveBeenCalled(); 54 + expect(token).toBe('bar'); 55 + }); 56 + 57 + it('returns nothing when apiKey function is undefined', async () => { 58 + const accessToken = vi.fn().mockReturnValue('foo'); 59 + const token = await getAuthToken( 60 + { 61 + fn: 'apiKey', 62 + in: 'header', 63 + name: 'baz', 64 + }, 65 + { 66 + accessToken, 67 + }, 68 + ); 69 + expect(token).toBeUndefined(); 70 + }); 71 + }); 72 + 73 + describe('getParseAs', () => { 74 + const scenarios: Array<{ 75 + content: Parameters<typeof getParseAs>[0]; 76 + parseAs: ReturnType<typeof getParseAs>; 77 + }> = [ 78 + { 79 + content: null, 80 + parseAs: undefined, 81 + }, 82 + { 83 + content: 'application/json', 84 + parseAs: 'json', 85 + }, 86 + { 87 + content: 'application/ld+json', 88 + parseAs: 'json', 89 + }, 90 + { 91 + content: 'application/ld+json;charset=utf-8', 92 + parseAs: 'json', 93 + }, 94 + { 95 + content: 'application/ld+json; charset=utf-8', 96 + parseAs: 'json', 97 + }, 98 + { 99 + content: 'multipart/form-data', 100 + parseAs: 'formData', 101 + }, 102 + { 103 + content: 'application/*', 104 + parseAs: 'blob', 105 + }, 106 + { 107 + content: 'audio/*', 108 + parseAs: 'blob', 109 + }, 110 + { 111 + content: 'image/*', 112 + parseAs: 'blob', 113 + }, 114 + { 115 + content: 'video/*', 116 + parseAs: 'blob', 117 + }, 118 + { 119 + content: 'text/*', 120 + parseAs: 'text', 121 + }, 122 + { 123 + content: 'unsupported', 124 + parseAs: undefined, 125 + }, 126 + ]; 127 + 128 + it.each(scenarios)( 129 + 'detects $content as $parseAs', 130 + async ({ content, parseAs }) => { 131 + expect(getParseAs(content)).toEqual(parseAs); 132 + }, 133 + ); 134 + }); 135 + 136 + describe('setAuthParams', () => { 137 + it('sets access token in headers', async () => { 138 + const accessToken = vi.fn().mockReturnValue('foo'); 139 + const apiKey = vi.fn().mockReturnValue('bar'); 140 + const headers = new Headers(); 141 + const query: Record<any, unknown> = {}; 142 + await setAuthParams({ 143 + accessToken, 144 + apiKey, 145 + headers, 146 + query, 147 + security: [ 148 + { 149 + fn: 'accessToken', 150 + in: 'header', 151 + name: 'baz', 152 + }, 153 + ], 154 + }); 155 + expect(accessToken).toHaveBeenCalled(); 156 + expect(headers.get('baz')).toBe('Bearer foo'); 157 + expect(Object.keys(query).length).toBe(0); 158 + }); 159 + 160 + it('sets access token in query', async () => { 161 + const accessToken = vi.fn().mockReturnValue('foo'); 162 + const apiKey = vi.fn().mockReturnValue('bar'); 163 + const headers = new Headers(); 164 + const query: Record<any, unknown> = {}; 165 + await setAuthParams({ 166 + accessToken, 167 + apiKey, 168 + headers, 169 + query, 170 + security: [ 171 + { 172 + fn: 'accessToken', 173 + in: 'query', 174 + name: 'baz', 175 + }, 176 + ], 177 + }); 178 + expect(accessToken).toHaveBeenCalled(); 179 + expect(headers.get('baz')).toBeNull(); 180 + expect(query.baz).toBe('Bearer foo'); 181 + }); 182 + 183 + it('sets first scheme only', async () => { 184 + const accessToken = vi.fn().mockReturnValue('foo'); 185 + const apiKey = vi.fn().mockReturnValue('bar'); 186 + const headers = new Headers(); 187 + const query: Record<any, unknown> = {}; 188 + await setAuthParams({ 189 + accessToken, 190 + apiKey, 191 + headers, 192 + query, 193 + security: [ 194 + { 195 + fn: 'accessToken', 196 + in: 'header', 197 + name: 'baz', 198 + }, 199 + { 200 + fn: 'accessToken', 201 + in: 'query', 202 + name: 'baz', 203 + }, 204 + ], 205 + }); 206 + expect(accessToken).toHaveBeenCalled(); 207 + expect(headers.get('baz')).toBe('Bearer foo'); 208 + expect(Object.keys(query).length).toBe(0); 209 + }); 210 + 211 + it('sets first scheme with token', async () => { 212 + const accessToken = vi.fn().mockReturnValue('foo'); 213 + const apiKey = vi.fn().mockReturnValue(undefined); 214 + const headers = new Headers(); 215 + const query: Record<any, unknown> = {}; 216 + await setAuthParams({ 217 + accessToken, 218 + apiKey, 219 + headers, 220 + query, 221 + security: [ 222 + { 223 + fn: 'apiKey', 224 + in: 'header', 225 + name: 'baz', 226 + }, 227 + { 228 + fn: 'accessToken', 229 + in: 'query', 230 + name: 'baz', 231 + }, 232 + ], 233 + }); 234 + expect(accessToken).toHaveBeenCalled(); 235 + expect(headers.get('baz')).toBeNull(); 236 + expect(query.baz).toBe('Bearer foo'); 237 + }); 238 + });
+11 -18
packages/client-fetch/src/index.ts
··· 1 1 import type { Client, Config, RequestOptions } from './types'; 2 2 import { 3 + buildUrl, 3 4 createConfig, 4 5 createInterceptors, 5 - createQuerySerializer, 6 6 getParseAs, 7 - getUrl, 8 7 mergeConfigs, 9 8 mergeHeaders, 9 + setAuthParams, 10 10 } from './utils'; 11 11 12 12 type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { ··· 24 24 return getConfig(); 25 25 }; 26 26 27 - const buildUrl: Client['buildUrl'] = (options) => { 28 - const url = getUrl({ 29 - baseUrl: options.baseUrl ?? '', 30 - path: options.path, 31 - query: options.query, 32 - querySerializer: 33 - typeof options.querySerializer === 'function' 34 - ? options.querySerializer 35 - : createQuerySerializer(options.querySerializer), 36 - url: options.url, 37 - }); 38 - return url; 39 - }; 40 - 41 27 const interceptors = createInterceptors< 42 28 Request, 43 29 Response, ··· 53 39 fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 54 40 headers: mergeHeaders(_config.headers, options.headers), 55 41 }; 42 + 43 + if (opts.security) { 44 + await setAuthParams({ 45 + ...opts, 46 + security: opts.security, 47 + }); 48 + } 49 + 56 50 if (opts.body && opts.bodySerializer) { 57 51 opts.body = opts.bodySerializer(opts.body); 58 52 } ··· 74 68 request = await fn(request, opts); 75 69 } 76 70 77 - const _fetch = opts.fetch!; 78 - let response = await _fetch(request); 71 + let response = await opts.fetch(request); 79 72 80 73 for (const fn of interceptors.response._fns) { 81 74 response = await fn(response, request, opts);
+24
packages/client-fetch/src/types.ts
··· 10 10 export interface Config<ThrowOnError extends boolean = boolean> 11 11 extends Omit<RequestInit, 'body' | 'headers' | 'method'> { 12 12 /** 13 + * Access token or a function returning access token. The resolved token 14 + * will be added to request headers where it's required. 15 + */ 16 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 17 + /** 18 + * API key or a function returning API key. The resolved key will be added 19 + * to the request payload as required. 20 + */ 21 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 22 + /** 13 23 * Base URL for all requests made by this client. 24 + * 14 25 * @default '' 15 26 */ 16 27 baseUrl?: string; ··· 22 33 /** 23 34 * Fetch API implementation. You can use this option to provide a custom 24 35 * fetch instance. 36 + * 25 37 * @default globalThis.fetch 26 38 */ 27 39 fetch?: (request: Request) => ReturnType<typeof fetch>; ··· 63 75 * will infer the appropriate method from the `Content-Type` response header. 64 76 * You can override this behavior with any of the {@link Body} methods. 65 77 * Select `stream` if you don't want to parse response data at all. 78 + * 66 79 * @default 'auto' 67 80 */ 68 81 parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; ··· 82 95 responseTransformer?: (data: unknown) => Promise<unknown>; 83 96 /** 84 97 * Throw an error instead of returning it in the response? 98 + * 85 99 * @default false 86 100 */ 87 101 throwOnError?: ThrowOnError; ··· 110 124 client?: Client; 111 125 path?: Record<string, unknown>; 112 126 query?: Record<string, unknown>; 127 + /** 128 + * Security mechanism(s) to use for the request. 129 + */ 130 + security?: ReadonlyArray<Security>; 113 131 url: Url; 114 132 } 115 133 ··· 132 150 response: Response; 133 151 } 134 152 >; 153 + 154 + export interface Security { 155 + fn: 'accessToken' | 'apiKey'; 156 + in: 'header' | 'query'; 157 + name: string; 158 + } 135 159 136 160 type MethodFn = < 137 161 Data = unknown,
+63 -2
packages/client-fetch/src/utils.ts
··· 1 - import type { Config } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 358 358 } 359 359 }; 360 360 361 + export const getAuthToken = async ( 362 + security: Security, 363 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 364 + ): Promise<string | undefined> => { 365 + if (security.fn === 'accessToken') { 366 + const token = 367 + typeof options.accessToken === 'function' 368 + ? await options.accessToken() 369 + : options.accessToken; 370 + return token ? `Bearer ${token}` : undefined; 371 + } 372 + 373 + if (security.fn === 'apiKey') { 374 + return typeof options.apiKey === 'function' 375 + ? await options.apiKey() 376 + : options.apiKey; 377 + } 378 + }; 379 + 380 + export const setAuthParams = async ({ 381 + security, 382 + ...options 383 + }: Pick<Required<RequestOptions>, 'security'> & 384 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 385 + headers: Headers; 386 + }) => { 387 + for (const scheme of security) { 388 + const token = await getAuthToken(scheme, options); 389 + 390 + if (!token) { 391 + continue; 392 + } 393 + 394 + if (scheme.in === 'header') { 395 + options.headers.set(scheme.name, token); 396 + } else if (scheme.in === 'query') { 397 + if (!options.query) { 398 + options.query = {}; 399 + } 400 + 401 + options.query[scheme.name] = token; 402 + } 403 + 404 + return; 405 + } 406 + }; 407 + 408 + export const buildUrl: Client['buildUrl'] = (options) => { 409 + const url = getUrl({ 410 + baseUrl: options.baseUrl ?? '', 411 + path: options.path, 412 + query: options.query, 413 + querySerializer: 414 + typeof options.querySerializer === 'function' 415 + ? options.querySerializer 416 + : createQuerySerializer(options.querySerializer), 417 + url: options.url, 418 + }); 419 + return url; 420 + }; 421 + 361 422 export const getUrl = ({ 362 423 baseUrl, 363 424 path, ··· 397 458 398 459 export const mergeHeaders = ( 399 460 ...headers: Array<Required<Config>['headers'] | undefined> 400 - ) => { 461 + ): Headers => { 401 462 const mergedHeaders = new Headers(); 402 463 for (const header of headers) { 403 464 if (!header || typeof header !== 'object') {
+1 -1
packages/client-fetch/test/index.test.ts packages/client-fetch/src/__tests__/index.test.ts
··· 1 1 import { describe, expect, it } from 'vitest'; 2 2 3 - import { createClient } from '../src/index'; 3 + import { createClient } from '../index'; 4 4 5 5 describe('buildUrl', () => { 6 6 const client = createClient();
+1
packages/openapi-ts/.gitignore
··· 3 3 .tsup 4 4 .tmp 5 5 junit.xml 6 + logs 6 7 node_modules 7 8 npm-debug.log* 8 9 temp
+2 -1
packages/openapi-ts/src/ir/ir.d.ts
··· 1 1 import type { JsonSchemaDraft2020_12 } from '../openApi/3.1.x/types/json-schema-draft-2020-12'; 2 + import type { SecuritySchemeObject } from '../openApi/3.1.x/types/spec'; 2 3 import type { IRMediaType } from './mediaType'; 3 4 4 5 export interface IR { ··· 36 37 parameters?: IRParametersObject; 37 38 path: keyof IRPathsObject; 38 39 responses?: IRResponsesObject; 40 + security?: ReadonlyArray<SecuritySchemeObject>; 39 41 // TODO: parser - add more properties 40 - // security?: ReadonlyArray<SecurityRequirementObject>; 41 42 // servers?: ReadonlyArray<ServerObject>; 42 43 summary?: string; 43 44 tags?: ReadonlyArray<string>;
+13
packages/openapi-ts/src/openApi/3.0.x/parser/index.ts
··· 6 6 PathItemObject, 7 7 PathsObject, 8 8 RequestBodyObject, 9 + SecuritySchemeObject, 9 10 } from '../types/spec'; 10 11 import { parseOperation } from './operation'; 11 12 import { ··· 18 19 19 20 export const parseV3_0_X = (context: IRContext<OpenApiV3_0_X>) => { 20 21 const operationIds = new Map<string, string>(); 22 + const securitySchemesMap = new Map<string, SecuritySchemeObject>(); 21 23 22 24 const excludeRegExp = context.config.input.exclude 23 25 ? new RegExp(context.config.input.exclude) ··· 35 37 36 38 // TODO: parser - handle more component types, old parser handles only parameters and schemas 37 39 if (context.spec.components) { 40 + for (const name in context.spec.components.securitySchemes) { 41 + const securityOrReference = context.spec.components.securitySchemes[name]; 42 + const securitySchemeObject = 43 + '$ref' in securityOrReference 44 + ? context.resolveRef<SecuritySchemeObject>(securityOrReference.$ref) 45 + : securityOrReference; 46 + securitySchemesMap.set(name, securitySchemeObject); 47 + } 48 + 38 49 for (const name in context.spec.components.parameters) { 39 50 const $ref = `#/components/parameters/${name}`; 40 51 if (!shouldProcessRef($ref)) { ··· 117 128 context, 118 129 parameters: finalPathItem.parameters, 119 130 }), 131 + security: context.spec.security, 120 132 servers: finalPathItem.servers, 121 133 summary: finalPathItem.summary, 122 134 }, 123 135 operationIds, 124 136 path: path as keyof PathsObject, 137 + securitySchemesMap, 125 138 }; 126 139 127 140 const $refDelete = `#/paths${path}/delete`;
+22 -2
packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts
··· 6 6 PathItemObject, 7 7 RequestBodyObject, 8 8 ResponseObject, 9 + SecuritySchemeObject, 9 10 } from '../types/spec'; 10 11 import { contentToSchema, mediaTypeObject } from './mediaType'; 11 12 import { paginationField } from './pagination'; ··· 65 66 method, 66 67 operation, 67 68 path, 69 + securitySchemesMap, 68 70 }: Pick<IROperationObject, 'method' | 'path'> & { 69 71 context: IRContext; 70 72 operation: Operation; 73 + securitySchemesMap: Map<string, SecuritySchemeObject>; 71 74 }): IROperationObject => { 72 75 const irOperation = initIrOperation({ method, operation, path }); 73 76 ··· 172 175 } 173 176 } 174 177 175 - // TODO: parser - handle security 176 - // baz: operation.security 178 + if (operation.security) { 179 + const securitySchemeObjects: Array<SecuritySchemeObject> = []; 180 + 181 + for (const securityRequirementObject of operation.security) { 182 + for (const name in securityRequirementObject) { 183 + const securitySchemeObject = securitySchemesMap.get(name); 184 + if (securitySchemeObject) { 185 + securitySchemeObjects.push(securitySchemeObject); 186 + } 187 + } 188 + } 189 + 190 + if (securitySchemeObjects.length) { 191 + irOperation.security = securitySchemeObjects; 192 + } 193 + } 177 194 178 195 // TODO: parser - handle servers 179 196 // qux: operation.servers ··· 187 204 operation, 188 205 operationIds, 189 206 path, 207 + securitySchemesMap, 190 208 }: { 191 209 context: IRContext; 192 210 method: Extract< ··· 196 214 operation: Operation; 197 215 operationIds: Map<string, string>; 198 216 path: keyof IRPathsObject; 217 + securitySchemesMap: Map<string, SecuritySchemeObject>; 199 218 }) => { 200 219 // TODO: parser - support throw on duplicate 201 220 if (operation.operationId) { ··· 230 249 method, 231 250 operation, 232 251 path, 252 + securitySchemesMap, 233 253 }); 234 254 };
+13
packages/openapi-ts/src/openApi/3.1.x/parser/index.ts
··· 6 6 PathItemObject, 7 7 PathsObject, 8 8 RequestBodyObject, 9 + SecuritySchemeObject, 9 10 } from '../types/spec'; 10 11 import { parseOperation } from './operation'; 11 12 import { ··· 18 19 19 20 export const parseV3_1_X = (context: IRContext<OpenApiV3_1_X>) => { 20 21 const operationIds = new Map<string, string>(); 22 + const securitySchemesMap = new Map<string, SecuritySchemeObject>(); 21 23 22 24 const excludeRegExp = context.config.input.exclude 23 25 ? new RegExp(context.config.input.exclude) ··· 35 37 36 38 // TODO: parser - handle more component types, old parser handles only parameters and schemas 37 39 if (context.spec.components) { 40 + for (const name in context.spec.components.securitySchemes) { 41 + const securityOrReference = context.spec.components.securitySchemes[name]; 42 + const securitySchemeObject = 43 + '$ref' in securityOrReference 44 + ? context.resolveRef<SecuritySchemeObject>(securityOrReference.$ref) 45 + : securityOrReference; 46 + securitySchemesMap.set(name, securitySchemeObject); 47 + } 48 + 38 49 for (const name in context.spec.components.parameters) { 39 50 const $ref = `#/components/parameters/${name}`; 40 51 if (!shouldProcessRef($ref)) { ··· 110 121 context, 111 122 parameters: finalPathItem.parameters, 112 123 }), 124 + security: context.spec.security, 113 125 servers: finalPathItem.servers, 114 126 summary: finalPathItem.summary, 115 127 }, 116 128 operationIds, 117 129 path: path as keyof PathsObject, 130 + securitySchemesMap, 118 131 }; 119 132 120 133 const $refDelete = `#/paths${path}/delete`;
+22 -2
packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts
··· 6 6 PathItemObject, 7 7 RequestBodyObject, 8 8 ResponseObject, 9 + SecuritySchemeObject, 9 10 } from '../types/spec'; 10 11 import { contentToSchema, mediaTypeObject } from './mediaType'; 11 12 import { paginationField } from './pagination'; ··· 65 66 method, 66 67 operation, 67 68 path, 69 + securitySchemesMap, 68 70 }: Pick<IROperationObject, 'method' | 'path'> & { 69 71 context: IRContext; 70 72 operation: Operation; 73 + securitySchemesMap: Map<string, SecuritySchemeObject>; 71 74 }): IROperationObject => { 72 75 const irOperation = initIrOperation({ method, operation, path }); 73 76 ··· 157 160 } 158 161 } 159 162 160 - // TODO: parser - handle security 161 - // baz: operation.security 163 + if (operation.security) { 164 + const securitySchemeObjects: Array<SecuritySchemeObject> = []; 165 + 166 + for (const securityRequirementObject of operation.security) { 167 + for (const name in securityRequirementObject) { 168 + const securitySchemeObject = securitySchemesMap.get(name); 169 + if (securitySchemeObject) { 170 + securitySchemeObjects.push(securitySchemeObject); 171 + } 172 + } 173 + } 174 + 175 + if (securitySchemeObjects.length) { 176 + irOperation.security = securitySchemeObjects; 177 + } 178 + } 162 179 163 180 // TODO: parser - handle servers 164 181 // qux: operation.servers ··· 172 189 operation, 173 190 operationIds, 174 191 path, 192 + securitySchemesMap, 175 193 }: { 176 194 context: IRContext; 177 195 method: Extract< ··· 181 199 operation: Operation; 182 200 operationIds: Map<string, string>; 183 201 path: keyof IRPathsObject; 202 + securitySchemesMap: Map<string, SecuritySchemeObject>; 184 203 }) => { 185 204 // TODO: parser - support throw on duplicate 186 205 if (operation.operationId) { ··· 215 234 method, 216 235 operation, 217 236 path, 237 + securitySchemesMap, 218 238 }); 219 239 };
+1
packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts
··· 9 9 _handlerLegacy: handlerLegacy, 10 10 _optionalDependencies: ['@hey-api/transformers'], 11 11 asClass: false, 12 + auth: true, 12 13 name: '@hey-api/sdk', 13 14 operationId: true, 14 15 output: 'sdk',
+70 -6
packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
··· 77 77 const operationStatements = ({ 78 78 context, 79 79 operation, 80 + plugin, 80 81 }: { 81 82 context: IRContext; 82 83 operation: IROperationObject; 84 + plugin: Plugin.Instance<Config>; 83 85 }): Array<ts.Statement> => { 84 86 const file = context.file({ id: sdkId })!; 85 87 const sdkOutput = file.nameWithoutExtension(); ··· 175 177 // content type. currently impossible because successes do not contain 176 178 // header information 177 179 180 + if (operation.security && plugin.auth) { 181 + // TODO: parser - handle more security types 182 + // type copied from client packages 183 + const security: Array<{ 184 + fn: 'accessToken' | 'apiKey'; 185 + in: 'header' | 'query'; 186 + name: string; 187 + }> = []; 188 + 189 + for (const securitySchemeObject of operation.security) { 190 + if (securitySchemeObject.type === 'oauth2') { 191 + if (securitySchemeObject.flows.password) { 192 + security.push({ 193 + fn: 'accessToken', 194 + in: 'header', 195 + name: 'Authorization', 196 + }); 197 + } 198 + } else if (securitySchemeObject.type === 'apiKey') { 199 + // TODO: parser - support cookies auth 200 + if (securitySchemeObject.in !== 'cookie') { 201 + security.push({ 202 + fn: 'apiKey', 203 + in: securitySchemeObject.in, 204 + name: securitySchemeObject.name, 205 + }); 206 + } 207 + } else { 208 + console.warn( 209 + `❗️ SDK warning: security scheme isn't currently supported. Please open an issue if you'd like it added https://github.com/hey-api/openapi-ts/issues\n${JSON.stringify(securitySchemeObject, null, 2)}`, 210 + ); 211 + } 212 + } 213 + 214 + if (security.length) { 215 + requestOptions.push({ 216 + key: 'security', 217 + value: compiler.arrayLiteralExpression({ elements: security }), 218 + }); 219 + } 220 + } 221 + 178 222 requestOptions.push({ 179 223 key: 'url', 180 224 value: operation.path, ··· 248 292 ]; 249 293 }; 250 294 251 - const generateClassSdk = ({ context }: { context: IRContext }) => { 295 + const generateClassSdk = ({ 296 + context, 297 + plugin, 298 + }: { 299 + context: IRContext; 300 + plugin: Plugin.Instance<Config>; 301 + }) => { 252 302 const file = context.file({ id: sdkId })!; 253 303 const typesModule = file.relativePathToFile({ context, id: 'types' }); 254 304 ··· 292 342 }, 293 343 ], 294 344 returnType: undefined, 295 - statements: operationStatements({ context, operation }), 345 + statements: operationStatements({ 346 + context, 347 + operation, 348 + plugin, 349 + }), 296 350 types: [ 297 351 { 298 352 default: false, ··· 330 384 }); 331 385 }; 332 386 333 - const generateFlatSdk = ({ context }: { context: IRContext }) => { 387 + const generateFlatSdk = ({ 388 + context, 389 + plugin, 390 + }: { 391 + context: IRContext; 392 + plugin: Plugin.Instance<Config>; 393 + }) => { 334 394 const file = context.file({ id: sdkId })!; 335 395 const typesModule = file.relativePathToFile({ context, id: 'types' }); 336 396 ··· 366 426 }, 367 427 ], 368 428 returnType: undefined, 369 - statements: operationStatements({ context, operation }), 429 + statements: operationStatements({ 430 + context, 431 + operation, 432 + plugin, 433 + }), 370 434 types: [ 371 435 { 372 436 default: false, ··· 438 502 file.add(statement); 439 503 440 504 if (plugin.asClass) { 441 - generateClassSdk({ context }); 505 + generateClassSdk({ context, plugin }); 442 506 } else { 443 - generateFlatSdk({ context }); 507 + generateFlatSdk({ context, plugin }); 444 508 } 445 509 };
+18 -2
packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts
··· 11 11 * Note that by enabling this option, your SDKs will **NOT** 12 12 * support {@link https://developer.mozilla.org/docs/Glossary/Tree_shaking tree-shaking}. 13 13 * For this reason, it is disabled by default. 14 + * 14 15 * @default false 15 16 */ 16 17 asClass?: boolean; 17 18 /** 19 + * **This feature works only with the experimental parser** 20 + * 21 + * Should the generated functions contain auth mechanisms? You may want to 22 + * disable this option if you're handling auth yourself or defining it 23 + * globally on the client and want to reduce the size of generated code. 24 + * 25 + * @default true 26 + */ 27 + auth?: boolean; 28 + /** 29 + * **This feature works only with the legacy parser** 30 + * 18 31 * Filter endpoints to be included in the generated SDK. The provided 19 32 * string should be a regular expression where matched results will be 20 33 * included in the output. The input pattern this string will be tested 21 34 * against is `{method} {path}`. For example, you can match 22 35 * `POST /api/v1/foo` with `^POST /api/v1/foo$`. 23 - * 24 - * This option does not work with the experimental parser. 25 36 * 26 37 * @deprecated 27 38 */ ··· 39 50 // TODO: parser - rename operationId option to something like inferId?: boolean 40 51 /** 41 52 * Use operation ID to generate operation names? 53 + * 42 54 * @default true 43 55 */ 44 56 operationId?: boolean; 45 57 /** 46 58 * Name of the generated file. 59 + * 47 60 * @default 'sdk' 48 61 */ 49 62 output?: string; 50 63 /** 51 64 * Define shape of returned value from service calls 65 + * 52 66 * @default 'body' 67 + * 53 68 * @deprecated 54 69 */ 55 70 response?: 'body' | 'response'; ··· 58 73 * obtained from your OpenAPI specification tags. 59 74 * 60 75 * This option has no effect if `sdk.asClass` is `false`. 76 + * 61 77 * @default '{{name}}Service' 62 78 */ 63 79 serviceNameBuilder?: string;
+3 -2
packages/openapi-ts/src/plugins/@hey-api/typescript/types.d.ts
··· 39 39 */ 40 40 identifierCase?: Exclude<StringCase, 'SCREAMING_SNAKE_CASE'>; 41 41 /** 42 - * Include only types matching regular expression. 42 + * **This feature works only with the legacy parser** 43 43 * 44 - * This option does not work with the experimental parser. 44 + * Include only types matching regular expression. 45 45 * 46 46 * @deprecated 47 47 */ 48 48 include?: string; 49 49 /** 50 50 * Name of the generated file. 51 + * 51 52 * @default 'types' 52 53 */ 53 54 output?: string;
+39
packages/openapi-ts/test/3.0.x.test.ts
··· 402 402 }, 403 403 { 404 404 config: createConfig({ 405 + input: 'security-api-key.json', 406 + output: 'security-api-key', 407 + plugins: [ 408 + { 409 + auth: true, 410 + name: '@hey-api/sdk', 411 + }, 412 + ], 413 + }), 414 + description: 'generates SDK functions with auth (api key)', 415 + }, 416 + { 417 + config: createConfig({ 418 + input: 'security-oauth2.json', 419 + output: 'security-oauth2', 420 + plugins: [ 421 + { 422 + auth: true, 423 + name: '@hey-api/sdk', 424 + }, 425 + ], 426 + }), 427 + description: 'generates SDK functions with auth (oauth2)', 428 + }, 429 + { 430 + config: createConfig({ 431 + input: 'security-oauth2.json', 432 + output: 'security-false', 433 + plugins: [ 434 + { 435 + auth: false, 436 + name: '@hey-api/sdk', 437 + }, 438 + ], 439 + }), 440 + description: 'generates SDK functions without auth', 441 + }, 442 + { 443 + config: createConfig({ 405 444 input: 'transformers-all-of.yaml', 406 445 output: 'transformers-all-of', 407 446 plugins: ['@hey-api/transformers'],
+39
packages/openapi-ts/test/3.1.x.test.ts
··· 463 463 }, 464 464 { 465 465 config: createConfig({ 466 + input: 'security-api-key.json', 467 + output: 'security-api-key', 468 + plugins: [ 469 + { 470 + auth: true, 471 + name: '@hey-api/sdk', 472 + }, 473 + ], 474 + }), 475 + description: 'generates SDK functions with auth (api key)', 476 + }, 477 + { 478 + config: createConfig({ 479 + input: 'security-oauth2.json', 480 + output: 'security-oauth2', 481 + plugins: [ 482 + { 483 + auth: true, 484 + name: '@hey-api/sdk', 485 + }, 486 + ], 487 + }), 488 + description: 'generates SDK functions with auth (oauth2)', 489 + }, 490 + { 491 + config: createConfig({ 492 + input: 'security-oauth2.json', 493 + output: 'security-false', 494 + plugins: [ 495 + { 496 + auth: false, 497 + name: '@hey-api/sdk', 498 + }, 499 + ], 500 + }), 501 + description: 'generates SDK functions without auth', 502 + }, 503 + { 504 + config: createConfig({ 466 505 input: 'transformers-all-of.yaml', 467 506 output: 'transformers-all-of', 468 507 plugins: ['@hey-api/transformers'],
+3
packages/openapi-ts/test/__snapshots__/3.0.x/security-api-key/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+20
packages/openapi-ts/test/__snapshots__/3.0.x/security-api-key/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + security: [ 12 + { 13 + fn: 'apiKey', 14 + in: 'query', 15 + name: 'foo' 16 + } 17 + ], 18 + url: '/foo' 19 + }); 20 + };
+15
packages/openapi-ts/test/__snapshots__/3.0.x/security-api-key/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+3
packages/openapi-ts/test/__snapshots__/3.0.x/security-false/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+13
packages/openapi-ts/test/__snapshots__/3.0.x/security-false/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + url: '/foo' 12 + }); 13 + };
+15
packages/openapi-ts/test/__snapshots__/3.0.x/security-false/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+3
packages/openapi-ts/test/__snapshots__/3.0.x/security-oauth2/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+20
packages/openapi-ts/test/__snapshots__/3.0.x/security-oauth2/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + security: [ 12 + { 13 + fn: 'accessToken', 14 + in: 'header', 15 + name: 'Authorization' 16 + } 17 + ], 18 + url: '/foo' 19 + }); 20 + };
+15
packages/openapi-ts/test/__snapshots__/3.0.x/security-oauth2/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+3
packages/openapi-ts/test/__snapshots__/3.1.x/security-api-key/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+20
packages/openapi-ts/test/__snapshots__/3.1.x/security-api-key/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + security: [ 12 + { 13 + fn: 'apiKey', 14 + in: 'query', 15 + name: 'foo' 16 + } 17 + ], 18 + url: '/foo' 19 + }); 20 + };
+15
packages/openapi-ts/test/__snapshots__/3.1.x/security-api-key/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+3
packages/openapi-ts/test/__snapshots__/3.1.x/security-false/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+13
packages/openapi-ts/test/__snapshots__/3.1.x/security-false/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + url: '/foo' 12 + }); 13 + };
+15
packages/openapi-ts/test/__snapshots__/3.1.x/security-false/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+3
packages/openapi-ts/test/__snapshots__/3.1.x/security-oauth2/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen'; 3 + export * from './sdk.gen';
+20
packages/openapi-ts/test/__snapshots__/3.1.x/security-oauth2/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-fetch'; 4 + import type { GetFooData } from './types.gen'; 5 + 6 + export const client = createClient(createConfig()); 7 + 8 + export const getFoo = <ThrowOnError extends boolean = false>(options?: Options<GetFooData, ThrowOnError>) => { 9 + return (options?.client ?? client).get<unknown, unknown, ThrowOnError>({ 10 + ...options, 11 + security: [ 12 + { 13 + fn: 'accessToken', 14 + in: 'header', 15 + name: 'Authorization' 16 + } 17 + ], 18 + url: '/foo' 19 + }); 20 + };
+15
packages/openapi-ts/test/__snapshots__/3.1.x/security-oauth2/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type GetFooData = { 4 + body?: never; 5 + path?: never; 6 + query?: never; 7 + url: '/foo'; 8 + }; 9 + 10 + export type GetFooResponses = { 11 + /** 12 + * OK 13 + */ 14 + 200: unknown; 15 + };
+19 -8
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap
··· 2 2 import axios from 'axios'; 3 3 4 4 import type { Client, Config } from './types'; 5 - import { createConfig, getUrl, mergeConfigs, mergeHeaders } from './utils'; 5 + import { 6 + createConfig, 7 + getUrl, 8 + mergeConfigs, 9 + mergeHeaders, 10 + setAuthParams, 11 + } from './utils'; 6 12 7 13 export const createClient = (config: Config): Client => { 8 14 let _config = mergeConfigs(createConfig(), config); ··· 27 33 const opts = { 28 34 ..._config, 29 35 ...options, 30 - headers: mergeHeaders( 31 - _config.headers, 32 - options.headers, 33 - ) as RawAxiosRequestHeaders, 36 + axios: options.axios ?? _config.axios ?? instance, 37 + headers: mergeHeaders(_config.headers, options.headers), 34 38 }; 39 + 40 + if (opts.security) { 41 + await setAuthParams({ 42 + ...opts, 43 + security: opts.security, 44 + }); 45 + } 46 + 35 47 if (opts.body && opts.bodySerializer) { 36 48 opts.body = opts.bodySerializer(opts.body); 37 49 } ··· 41 53 url: opts.url, 42 54 }); 43 55 44 - const _axios = opts.axios || instance; 45 - 46 56 try { 47 - const response = await _axios({ 57 + const response = await opts.axios({ 48 58 ...opts, 49 59 data: opts.body, 60 + headers: opts.headers as RawAxiosRequestHeaders, 50 61 params: opts.query, 51 62 url, 52 63 });
+22
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap
··· 13 13 export interface Config<ThrowOnError extends boolean = boolean> 14 14 extends Omit<CreateAxiosDefaults, 'headers'> { 15 15 /** 16 + * Access token or a function returning access token. The resolved token will 17 + * be added to request payload as required. 18 + */ 19 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 20 + /** 21 + * API key or a function returning API key. The resolved key will be added 22 + * to the request payload as required. 23 + */ 24 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 25 + /** 16 26 * Axios implementation. You can use this option to provide a custom 17 27 * Axios instance. 28 + * 18 29 * @default axios 19 30 */ 20 31 axios?: AxiosStatic; ··· 64 75 responseTransformer?: (data: unknown) => Promise<unknown>; 65 76 /** 66 77 * Throw an error instead of returning it in the response? 78 + * 67 79 * @default false 68 80 */ 69 81 throwOnError?: ThrowOnError; ··· 87 99 client?: Client; 88 100 path?: Record<string, unknown>; 89 101 query?: Record<string, unknown>; 102 + /** 103 + * Security mechanism(s) to use for the request. 104 + */ 105 + security?: ReadonlyArray<Security>; 90 106 url: Url; 91 107 } 92 108 ··· 100 116 | (AxiosResponse<Data> & { error: undefined }) 101 117 | (AxiosError<TError> & { data: undefined; error: TError }) 102 118 >; 119 + 120 + export interface Security { 121 + fn: 'accessToken' | 'apiKey'; 122 + in: 'header' | 'query'; 123 + name: string; 124 + } 103 125 104 126 type MethodFn = < 105 127 Data = unknown,
+50 -5
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/utils.ts.snap
··· 1 - import type { Config } from './types'; 1 + import type { Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 250 250 return url; 251 251 }; 252 252 253 + export const getAuthToken = async ( 254 + security: Security, 255 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 256 + ): Promise<string | undefined> => { 257 + if (security.fn === 'accessToken') { 258 + const token = 259 + typeof options.accessToken === 'function' 260 + ? await options.accessToken() 261 + : options.accessToken; 262 + return token ? `Bearer ${token}` : undefined; 263 + } 264 + 265 + if (security.fn === 'apiKey') { 266 + return typeof options.apiKey === 'function' 267 + ? await options.apiKey() 268 + : options.apiKey; 269 + } 270 + }; 271 + 272 + export const setAuthParams = async ({ 273 + security, 274 + ...options 275 + }: Pick<Required<RequestOptions>, 'security'> & 276 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 277 + headers: Record<any, unknown>; 278 + }) => { 279 + for (const scheme of security) { 280 + const token = await getAuthToken(scheme, options); 281 + 282 + if (!token) { 283 + continue; 284 + } 285 + 286 + if (scheme.in === 'header') { 287 + options.headers[scheme.name] = token; 288 + } else if (scheme.in === 'query') { 289 + if (!options.query) { 290 + options.query = {}; 291 + } 292 + 293 + options.query[scheme.name] = token; 294 + } 295 + 296 + return; 297 + } 298 + }; 299 + 253 300 export const getUrl = ({ 254 301 path, 255 302 url, ··· 278 325 279 326 export const mergeHeaders = ( 280 327 ...headers: Array<Required<Config>['headers'] | undefined> 281 - ): Required<Config>['headers'] => { 282 - const mergedHeaders: Required<Config>['headers'] = {}; 328 + ): Record<any, unknown> => { 329 + const mergedHeaders: Record<any, unknown> = {}; 283 330 for (const header of headers) { 284 331 if (!header || typeof header !== 'object') { 285 332 continue; ··· 289 336 290 337 for (const [key, value] of iterator) { 291 338 if (value === null) { 292 - // @ts-expect-error 293 339 delete mergedHeaders[key]; 294 340 } else if (Array.isArray(value)) { 295 341 for (const v of value) { ··· 299 345 } else if (value !== undefined) { 300 346 // assume object headers are meant to be JSON stringified, i.e. their 301 347 // content value in OpenAPI specification is 'application/json' 302 - // @ts-expect-error 303 348 mergedHeaders[key] = 304 349 typeof value === 'object' ? JSON.stringify(value) : (value as string); 305 350 }
+19 -8
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap
··· 2 2 import axios from 'axios'; 3 3 4 4 import type { Client, Config } from './types'; 5 - import { createConfig, getUrl, mergeConfigs, mergeHeaders } from './utils'; 5 + import { 6 + createConfig, 7 + getUrl, 8 + mergeConfigs, 9 + mergeHeaders, 10 + setAuthParams, 11 + } from './utils'; 6 12 7 13 export const createClient = (config: Config): Client => { 8 14 let _config = mergeConfigs(createConfig(), config); ··· 27 33 const opts = { 28 34 ..._config, 29 35 ...options, 30 - headers: mergeHeaders( 31 - _config.headers, 32 - options.headers, 33 - ) as RawAxiosRequestHeaders, 36 + axios: options.axios ?? _config.axios ?? instance, 37 + headers: mergeHeaders(_config.headers, options.headers), 34 38 }; 39 + 40 + if (opts.security) { 41 + await setAuthParams({ 42 + ...opts, 43 + security: opts.security, 44 + }); 45 + } 46 + 35 47 if (opts.body && opts.bodySerializer) { 36 48 opts.body = opts.bodySerializer(opts.body); 37 49 } ··· 41 53 url: opts.url, 42 54 }); 43 55 44 - const _axios = opts.axios || instance; 45 - 46 56 try { 47 - const response = await _axios({ 57 + const response = await opts.axios({ 48 58 ...opts, 49 59 data: opts.body, 60 + headers: opts.headers as RawAxiosRequestHeaders, 50 61 params: opts.query, 51 62 url, 52 63 });
+22
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap
··· 13 13 export interface Config<ThrowOnError extends boolean = boolean> 14 14 extends Omit<CreateAxiosDefaults, 'headers'> { 15 15 /** 16 + * Access token or a function returning access token. The resolved token will 17 + * be added to request payload as required. 18 + */ 19 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 20 + /** 21 + * API key or a function returning API key. The resolved key will be added 22 + * to the request payload as required. 23 + */ 24 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 25 + /** 16 26 * Axios implementation. You can use this option to provide a custom 17 27 * Axios instance. 28 + * 18 29 * @default axios 19 30 */ 20 31 axios?: AxiosStatic; ··· 64 75 responseTransformer?: (data: unknown) => Promise<unknown>; 65 76 /** 66 77 * Throw an error instead of returning it in the response? 78 + * 67 79 * @default false 68 80 */ 69 81 throwOnError?: ThrowOnError; ··· 87 99 client?: Client; 88 100 path?: Record<string, unknown>; 89 101 query?: Record<string, unknown>; 102 + /** 103 + * Security mechanism(s) to use for the request. 104 + */ 105 + security?: ReadonlyArray<Security>; 90 106 url: Url; 91 107 } 92 108 ··· 100 116 | (AxiosResponse<Data> & { error: undefined }) 101 117 | (AxiosError<TError> & { data: undefined; error: TError }) 102 118 >; 119 + 120 + export interface Security { 121 + fn: 'accessToken' | 'apiKey'; 122 + in: 'header' | 'query'; 123 + name: string; 124 + } 103 125 104 126 type MethodFn = < 105 127 Data = unknown,
+50 -5
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/utils.ts.snap
··· 1 - import type { Config } from './types'; 1 + import type { Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 250 250 return url; 251 251 }; 252 252 253 + export const getAuthToken = async ( 254 + security: Security, 255 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 256 + ): Promise<string | undefined> => { 257 + if (security.fn === 'accessToken') { 258 + const token = 259 + typeof options.accessToken === 'function' 260 + ? await options.accessToken() 261 + : options.accessToken; 262 + return token ? `Bearer ${token}` : undefined; 263 + } 264 + 265 + if (security.fn === 'apiKey') { 266 + return typeof options.apiKey === 'function' 267 + ? await options.apiKey() 268 + : options.apiKey; 269 + } 270 + }; 271 + 272 + export const setAuthParams = async ({ 273 + security, 274 + ...options 275 + }: Pick<Required<RequestOptions>, 'security'> & 276 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 277 + headers: Record<any, unknown>; 278 + }) => { 279 + for (const scheme of security) { 280 + const token = await getAuthToken(scheme, options); 281 + 282 + if (!token) { 283 + continue; 284 + } 285 + 286 + if (scheme.in === 'header') { 287 + options.headers[scheme.name] = token; 288 + } else if (scheme.in === 'query') { 289 + if (!options.query) { 290 + options.query = {}; 291 + } 292 + 293 + options.query[scheme.name] = token; 294 + } 295 + 296 + return; 297 + } 298 + }; 299 + 253 300 export const getUrl = ({ 254 301 path, 255 302 url, ··· 278 325 279 326 export const mergeHeaders = ( 280 327 ...headers: Array<Required<Config>['headers'] | undefined> 281 - ): Required<Config>['headers'] => { 282 - const mergedHeaders: Required<Config>['headers'] = {}; 328 + ): Record<any, unknown> => { 329 + const mergedHeaders: Record<any, unknown> = {}; 283 330 for (const header of headers) { 284 331 if (!header || typeof header !== 'object') { 285 332 continue; ··· 289 336 290 337 for (const [key, value] of iterator) { 291 338 if (value === null) { 292 - // @ts-expect-error 293 339 delete mergedHeaders[key]; 294 340 } else if (Array.isArray(value)) { 295 341 for (const v of value) { ··· 299 345 } else if (value !== undefined) { 300 346 // assume object headers are meant to be JSON stringified, i.e. their 301 347 // content value in OpenAPI specification is 'application/json' 302 - // @ts-expect-error 303 348 mergedHeaders[key] = 304 349 typeof value === 'object' ? JSON.stringify(value) : (value as string); 305 350 }
+11 -18
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/index.ts.snap
··· 1 1 import type { Client, Config, RequestOptions } from './types'; 2 2 import { 3 + buildUrl, 3 4 createConfig, 4 5 createInterceptors, 5 - createQuerySerializer, 6 6 getParseAs, 7 - getUrl, 8 7 mergeConfigs, 9 8 mergeHeaders, 9 + setAuthParams, 10 10 } from './utils'; 11 11 12 12 type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { ··· 24 24 return getConfig(); 25 25 }; 26 26 27 - const buildUrl: Client['buildUrl'] = (options) => { 28 - const url = getUrl({ 29 - baseUrl: options.baseUrl ?? '', 30 - path: options.path, 31 - query: options.query, 32 - querySerializer: 33 - typeof options.querySerializer === 'function' 34 - ? options.querySerializer 35 - : createQuerySerializer(options.querySerializer), 36 - url: options.url, 37 - }); 38 - return url; 39 - }; 40 - 41 27 const interceptors = createInterceptors< 42 28 Request, 43 29 Response, ··· 53 39 fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 54 40 headers: mergeHeaders(_config.headers, options.headers), 55 41 }; 42 + 43 + if (opts.security) { 44 + await setAuthParams({ 45 + ...opts, 46 + security: opts.security, 47 + }); 48 + } 49 + 56 50 if (opts.body && opts.bodySerializer) { 57 51 opts.body = opts.bodySerializer(opts.body); 58 52 } ··· 74 68 request = await fn(request, opts); 75 69 } 76 70 77 - const _fetch = opts.fetch!; 78 - let response = await _fetch(request); 71 + let response = await opts.fetch(request); 79 72 80 73 for (const fn of interceptors.response._fns) { 81 74 response = await fn(response, request, opts);
+24
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/types.ts.snap
··· 10 10 export interface Config<ThrowOnError extends boolean = boolean> 11 11 extends Omit<RequestInit, 'body' | 'headers' | 'method'> { 12 12 /** 13 + * Access token or a function returning access token. The resolved token 14 + * will be added to request headers where it's required. 15 + */ 16 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 17 + /** 18 + * API key or a function returning API key. The resolved key will be added 19 + * to the request payload as required. 20 + */ 21 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 22 + /** 13 23 * Base URL for all requests made by this client. 24 + * 14 25 * @default '' 15 26 */ 16 27 baseUrl?: string; ··· 22 33 /** 23 34 * Fetch API implementation. You can use this option to provide a custom 24 35 * fetch instance. 36 + * 25 37 * @default globalThis.fetch 26 38 */ 27 39 fetch?: (request: Request) => ReturnType<typeof fetch>; ··· 63 75 * will infer the appropriate method from the `Content-Type` response header. 64 76 * You can override this behavior with any of the {@link Body} methods. 65 77 * Select `stream` if you don't want to parse response data at all. 78 + * 66 79 * @default 'auto' 67 80 */ 68 81 parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; ··· 82 95 responseTransformer?: (data: unknown) => Promise<unknown>; 83 96 /** 84 97 * Throw an error instead of returning it in the response? 98 + * 85 99 * @default false 86 100 */ 87 101 throwOnError?: ThrowOnError; ··· 110 124 client?: Client; 111 125 path?: Record<string, unknown>; 112 126 query?: Record<string, unknown>; 127 + /** 128 + * Security mechanism(s) to use for the request. 129 + */ 130 + security?: ReadonlyArray<Security>; 113 131 url: Url; 114 132 } 115 133 ··· 132 150 response: Response; 133 151 } 134 152 >; 153 + 154 + export interface Security { 155 + fn: 'accessToken' | 'apiKey'; 156 + in: 'header' | 'query'; 157 + name: string; 158 + } 135 159 136 160 type MethodFn = < 137 161 Data = unknown,
+63 -2
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/utils.ts.snap
··· 1 - import type { Config } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 358 358 } 359 359 }; 360 360 361 + export const getAuthToken = async ( 362 + security: Security, 363 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 364 + ): Promise<string | undefined> => { 365 + if (security.fn === 'accessToken') { 366 + const token = 367 + typeof options.accessToken === 'function' 368 + ? await options.accessToken() 369 + : options.accessToken; 370 + return token ? `Bearer ${token}` : undefined; 371 + } 372 + 373 + if (security.fn === 'apiKey') { 374 + return typeof options.apiKey === 'function' 375 + ? await options.apiKey() 376 + : options.apiKey; 377 + } 378 + }; 379 + 380 + export const setAuthParams = async ({ 381 + security, 382 + ...options 383 + }: Pick<Required<RequestOptions>, 'security'> & 384 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 385 + headers: Headers; 386 + }) => { 387 + for (const scheme of security) { 388 + const token = await getAuthToken(scheme, options); 389 + 390 + if (!token) { 391 + continue; 392 + } 393 + 394 + if (scheme.in === 'header') { 395 + options.headers.set(scheme.name, token); 396 + } else if (scheme.in === 'query') { 397 + if (!options.query) { 398 + options.query = {}; 399 + } 400 + 401 + options.query[scheme.name] = token; 402 + } 403 + 404 + return; 405 + } 406 + }; 407 + 408 + export const buildUrl: Client['buildUrl'] = (options) => { 409 + const url = getUrl({ 410 + baseUrl: options.baseUrl ?? '', 411 + path: options.path, 412 + query: options.query, 413 + querySerializer: 414 + typeof options.querySerializer === 'function' 415 + ? options.querySerializer 416 + : createQuerySerializer(options.querySerializer), 417 + url: options.url, 418 + }); 419 + return url; 420 + }; 421 + 361 422 export const getUrl = ({ 362 423 baseUrl, 363 424 path, ··· 397 458 398 459 export const mergeHeaders = ( 399 460 ...headers: Array<Required<Config>['headers'] | undefined> 400 - ) => { 461 + ): Headers => { 401 462 const mergedHeaders = new Headers(); 402 463 for (const header of headers) { 403 464 if (!header || typeof header !== 'object') {
+11 -18
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/index.ts.snap
··· 1 1 import type { Client, Config, RequestOptions } from './types'; 2 2 import { 3 + buildUrl, 3 4 createConfig, 4 5 createInterceptors, 5 - createQuerySerializer, 6 6 getParseAs, 7 - getUrl, 8 7 mergeConfigs, 9 8 mergeHeaders, 9 + setAuthParams, 10 10 } from './utils'; 11 11 12 12 type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { ··· 24 24 return getConfig(); 25 25 }; 26 26 27 - const buildUrl: Client['buildUrl'] = (options) => { 28 - const url = getUrl({ 29 - baseUrl: options.baseUrl ?? '', 30 - path: options.path, 31 - query: options.query, 32 - querySerializer: 33 - typeof options.querySerializer === 'function' 34 - ? options.querySerializer 35 - : createQuerySerializer(options.querySerializer), 36 - url: options.url, 37 - }); 38 - return url; 39 - }; 40 - 41 27 const interceptors = createInterceptors< 42 28 Request, 43 29 Response, ··· 53 39 fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 54 40 headers: mergeHeaders(_config.headers, options.headers), 55 41 }; 42 + 43 + if (opts.security) { 44 + await setAuthParams({ 45 + ...opts, 46 + security: opts.security, 47 + }); 48 + } 49 + 56 50 if (opts.body && opts.bodySerializer) { 57 51 opts.body = opts.bodySerializer(opts.body); 58 52 } ··· 74 68 request = await fn(request, opts); 75 69 } 76 70 77 - const _fetch = opts.fetch!; 78 - let response = await _fetch(request); 71 + let response = await opts.fetch(request); 79 72 80 73 for (const fn of interceptors.response._fns) { 81 74 response = await fn(response, request, opts);
+24
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/types.ts.snap
··· 10 10 export interface Config<ThrowOnError extends boolean = boolean> 11 11 extends Omit<RequestInit, 'body' | 'headers' | 'method'> { 12 12 /** 13 + * Access token or a function returning access token. The resolved token 14 + * will be added to request headers where it's required. 15 + */ 16 + accessToken?: (() => Promise<string | undefined>) | string | undefined; 17 + /** 18 + * API key or a function returning API key. The resolved key will be added 19 + * to the request payload as required. 20 + */ 21 + apiKey?: (() => Promise<string | undefined>) | string | undefined; 22 + /** 13 23 * Base URL for all requests made by this client. 24 + * 14 25 * @default '' 15 26 */ 16 27 baseUrl?: string; ··· 22 33 /** 23 34 * Fetch API implementation. You can use this option to provide a custom 24 35 * fetch instance. 36 + * 25 37 * @default globalThis.fetch 26 38 */ 27 39 fetch?: (request: Request) => ReturnType<typeof fetch>; ··· 63 75 * will infer the appropriate method from the `Content-Type` response header. 64 76 * You can override this behavior with any of the {@link Body} methods. 65 77 * Select `stream` if you don't want to parse response data at all. 78 + * 66 79 * @default 'auto' 67 80 */ 68 81 parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; ··· 82 95 responseTransformer?: (data: unknown) => Promise<unknown>; 83 96 /** 84 97 * Throw an error instead of returning it in the response? 98 + * 85 99 * @default false 86 100 */ 87 101 throwOnError?: ThrowOnError; ··· 110 124 client?: Client; 111 125 path?: Record<string, unknown>; 112 126 query?: Record<string, unknown>; 127 + /** 128 + * Security mechanism(s) to use for the request. 129 + */ 130 + security?: ReadonlyArray<Security>; 113 131 url: Url; 114 132 } 115 133 ··· 132 150 response: Response; 133 151 } 134 152 >; 153 + 154 + export interface Security { 155 + fn: 'accessToken' | 'apiKey'; 156 + in: 'header' | 'query'; 157 + name: string; 158 + } 135 159 136 160 type MethodFn = < 137 161 Data = unknown,
+63 -2
packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/utils.ts.snap
··· 1 - import type { Config } from './types'; 1 + import type { Client, Config, RequestOptions, Security } from './types'; 2 2 3 3 interface PathSerializer { 4 4 path: Record<string, unknown>; ··· 358 358 } 359 359 }; 360 360 361 + export const getAuthToken = async ( 362 + security: Security, 363 + options: Pick<RequestOptions, 'accessToken' | 'apiKey'>, 364 + ): Promise<string | undefined> => { 365 + if (security.fn === 'accessToken') { 366 + const token = 367 + typeof options.accessToken === 'function' 368 + ? await options.accessToken() 369 + : options.accessToken; 370 + return token ? `Bearer ${token}` : undefined; 371 + } 372 + 373 + if (security.fn === 'apiKey') { 374 + return typeof options.apiKey === 'function' 375 + ? await options.apiKey() 376 + : options.apiKey; 377 + } 378 + }; 379 + 380 + export const setAuthParams = async ({ 381 + security, 382 + ...options 383 + }: Pick<Required<RequestOptions>, 'security'> & 384 + Pick<RequestOptions, 'accessToken' | 'apiKey' | 'query'> & { 385 + headers: Headers; 386 + }) => { 387 + for (const scheme of security) { 388 + const token = await getAuthToken(scheme, options); 389 + 390 + if (!token) { 391 + continue; 392 + } 393 + 394 + if (scheme.in === 'header') { 395 + options.headers.set(scheme.name, token); 396 + } else if (scheme.in === 'query') { 397 + if (!options.query) { 398 + options.query = {}; 399 + } 400 + 401 + options.query[scheme.name] = token; 402 + } 403 + 404 + return; 405 + } 406 + }; 407 + 408 + export const buildUrl: Client['buildUrl'] = (options) => { 409 + const url = getUrl({ 410 + baseUrl: options.baseUrl ?? '', 411 + path: options.path, 412 + query: options.query, 413 + querySerializer: 414 + typeof options.querySerializer === 'function' 415 + ? options.querySerializer 416 + : createQuerySerializer(options.querySerializer), 417 + url: options.url, 418 + }); 419 + return url; 420 + }; 421 + 361 422 export const getUrl = ({ 362 423 baseUrl, 363 424 path, ··· 397 458 398 459 export const mergeHeaders = ( 399 460 ...headers: Array<Required<Config>['headers'] | undefined> 400 - ) => { 461 + ): Headers => { 401 462 const mergedHeaders = new Headers(); 402 463 for (const header of headers) { 403 464 if (!header || typeof header !== 'object') {
+7 -6
packages/openapi-ts/test/sample.cjs
··· 5 5 const config = { 6 6 client: { 7 7 // bundle: true, 8 - // name: '@hey-api/client-axios', 9 - name: '@hey-api/client-fetch', 8 + name: '@hey-api/client-axios', 9 + // name: '@hey-api/client-fetch', 10 10 }, 11 11 experimentalParser: true, 12 12 input: { 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.0.x/full.json', 16 + path: './test/spec/3.0.x/security-api-key.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 }, ··· 35 35 }, 36 36 { 37 37 // asClass: true, 38 + // auth: false, 38 39 // include... 39 - // name: '@hey-api/sdk', 40 + name: '@hey-api/sdk', 40 41 // operationId: false, 41 42 // serviceNameBuilder: '^Parameters', 42 43 }, ··· 50 51 enums: 'javascript', 51 52 // exportInlineEnums: true, 52 53 identifierCase: 'preserve', 53 - // name: '@hey-api/typescript', 54 + name: '@hey-api/typescript', 54 55 // tree: true, 55 56 }, 56 57 { ··· 60 61 // name: '@tanstack/vue-query', 61 62 }, 62 63 { 63 - name: 'zod', 64 + // name: 'zod', 64 65 }, 65 66 ], 66 67 // useOptions: false,
+32
packages/openapi-ts/test/spec/3.0.x/security-api-key.json
··· 1 + { 2 + "openapi": "3.0.4", 3 + "info": { 4 + "title": "OpenAPI 3.0.4 security api key example", 5 + "version": "1" 6 + }, 7 + "paths": { 8 + "/foo": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "OK" 13 + } 14 + }, 15 + "security": [ 16 + { 17 + "foo": [] 18 + } 19 + ] 20 + } 21 + } 22 + }, 23 + "components": { 24 + "securitySchemes": { 25 + "foo": { 26 + "in": "query", 27 + "name": "foo", 28 + "type": "apiKey" 29 + } 30 + } 31 + } 32 + }
+36
packages/openapi-ts/test/spec/3.0.x/security-oauth2.json
··· 1 + { 2 + "openapi": "3.0.4", 3 + "info": { 4 + "title": "OpenAPI 3.0.4 security oauth2 example", 5 + "version": "1" 6 + }, 7 + "paths": { 8 + "/foo": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "OK" 13 + } 14 + }, 15 + "security": [ 16 + { 17 + "foo": [] 18 + } 19 + ] 20 + } 21 + } 22 + }, 23 + "components": { 24 + "securitySchemes": { 25 + "foo": { 26 + "flows": { 27 + "password": { 28 + "scopes": {}, 29 + "tokenUrl": "/" 30 + } 31 + }, 32 + "type": "oauth2" 33 + } 34 + } 35 + } 36 + }
+32
packages/openapi-ts/test/spec/3.1.x/security-api-key.json
··· 1 + { 2 + "openapi": "3.1.1", 3 + "info": { 4 + "title": "OpenAPI 3.1.1 security api key example", 5 + "version": "1" 6 + }, 7 + "paths": { 8 + "/foo": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "OK" 13 + } 14 + }, 15 + "security": [ 16 + { 17 + "foo": [] 18 + } 19 + ] 20 + } 21 + } 22 + }, 23 + "components": { 24 + "securitySchemes": { 25 + "foo": { 26 + "in": "query", 27 + "name": "foo", 28 + "type": "apiKey" 29 + } 30 + } 31 + } 32 + }
+36
packages/openapi-ts/test/spec/3.1.x/security-oauth2.json
··· 1 + { 2 + "openapi": "3.1.1", 3 + "info": { 4 + "title": "OpenAPI 3.1.1 security oauth2 example", 5 + "version": "1" 6 + }, 7 + "paths": { 8 + "/foo": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "OK" 13 + } 14 + }, 15 + "security": [ 16 + { 17 + "foo": [] 18 + } 19 + ] 20 + } 21 + } 22 + }, 23 + "components": { 24 + "securitySchemes": { 25 + "foo": { 26 + "flows": { 27 + "password": { 28 + "scopes": {}, 29 + "tokenUrl": "/" 30 + } 31 + }, 32 + "type": "oauth2" 33 + } 34 + } 35 + } 36 + }