fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 412 lines 10 kB view raw
1import { getAuthToken } from './core/auth'; 2import type { QuerySerializer, QuerySerializerOptions } from './core/bodySerializer'; 3import { jsonBodySerializer } from './core/bodySerializer'; 4import { 5 serializeArrayParam, 6 serializeObjectParam, 7 serializePrimitiveParam, 8} from './core/pathSerializer'; 9import type { Client, ClientOptions, Config, RequestOptions } from './types'; 10 11interface PathSerializer { 12 path: Record<string, unknown>; 13 url: string; 14} 15 16const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 18type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19type MatrixStyle = 'label' | 'matrix' | 'simple'; 20type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 21 22const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 23 let url = _url; 24 const matches = _url.match(PATH_PARAM_RE); 25 if (matches) { 26 for (const match of matches) { 27 let explode = false; 28 let name = match.substring(1, match.length - 1); 29 let style: ArraySeparatorStyle = 'simple'; 30 31 if (name.endsWith('*')) { 32 explode = true; 33 name = name.substring(0, name.length - 1); 34 } 35 36 if (name.startsWith('.')) { 37 name = name.substring(1); 38 style = 'label'; 39 } else if (name.startsWith(';')) { 40 name = name.substring(1); 41 style = 'matrix'; 42 } 43 44 const value = path[name]; 45 46 if (value === undefined || value === null) { 47 continue; 48 } 49 50 if (Array.isArray(value)) { 51 url = url.replace(match, serializeArrayParam({ explode, name, style, value })); 52 continue; 53 } 54 55 if (typeof value === 'object') { 56 url = url.replace( 57 match, 58 serializeObjectParam({ 59 explode, 60 name, 61 style, 62 value: value as Record<string, unknown>, 63 valueOnly: true, 64 }), 65 ); 66 continue; 67 } 68 69 if (style === 'matrix') { 70 url = url.replace( 71 match, 72 `;${serializePrimitiveParam({ 73 name, 74 value: value as string, 75 })}`, 76 ); 77 continue; 78 } 79 80 const replaceValue = encodeURIComponent( 81 style === 'label' ? `.${value as string}` : (value as string), 82 ); 83 url = url.replace(match, replaceValue); 84 } 85 } 86 return url; 87}; 88 89export const createQuerySerializer = <T = unknown>({ 90 allowReserved, 91 array, 92 object, 93}: QuerySerializerOptions = {}) => { 94 const querySerializer = (queryParams: T) => { 95 const search: string[] = []; 96 if (queryParams && typeof queryParams === 'object') { 97 for (const name in queryParams) { 98 const value = queryParams[name]; 99 100 if (value === undefined || value === null) { 101 continue; 102 } 103 104 if (Array.isArray(value)) { 105 const serializedArray = serializeArrayParam({ 106 allowReserved, 107 explode: true, 108 name, 109 style: 'form', 110 value, 111 ...array, 112 }); 113 if (serializedArray) search.push(serializedArray); 114 } else if (typeof value === 'object') { 115 const serializedObject = serializeObjectParam({ 116 allowReserved, 117 explode: true, 118 name, 119 style: 'deepObject', 120 value: value as Record<string, unknown>, 121 ...object, 122 }); 123 if (serializedObject) search.push(serializedObject); 124 } else { 125 const serializedPrimitive = serializePrimitiveParam({ 126 allowReserved, 127 name, 128 value: value as string, 129 }); 130 if (serializedPrimitive) search.push(serializedPrimitive); 131 } 132 } 133 } 134 return search.join('&'); 135 }; 136 return querySerializer; 137}; 138 139/** 140 * Infers parseAs value from provided Content-Type header. 141 */ 142export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => { 143 if (!contentType) { 144 // If no Content-Type header is provided, the best we can do is return the raw response body, 145 // which is effectively the same as the 'stream' option. 146 return 'stream'; 147 } 148 149 const cleanContent = contentType.split(';')[0]?.trim(); 150 151 if (!cleanContent) { 152 return; 153 } 154 155 if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { 156 return 'json'; 157 } 158 159 if (cleanContent === 'multipart/form-data') { 160 return 'formData'; 161 } 162 163 if ( 164 ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) 165 ) { 166 return 'blob'; 167 } 168 169 if (cleanContent.startsWith('text/')) { 170 return 'text'; 171 } 172 173 return; 174}; 175 176const checkForExistence = ( 177 options: Pick<RequestOptions, 'auth' | 'query'> & { 178 headers: Headers; 179 }, 180 name?: string, 181): boolean => { 182 if (!name) { 183 return false; 184 } 185 if ( 186 options.headers.has(name) || 187 options.query?.[name] || 188 options.headers.get('Cookie')?.includes(`${name}=`) 189 ) { 190 return true; 191 } 192 return false; 193}; 194 195export const setAuthParams = async ({ 196 security, 197 ...options 198}: Pick<Required<RequestOptions>, 'security'> & 199 Pick<RequestOptions, 'auth' | 'query'> & { 200 headers: Headers; 201 }) => { 202 for (const auth of security) { 203 if (checkForExistence(options, auth.name)) { 204 continue; 205 } 206 207 const token = await getAuthToken(auth, options.auth); 208 209 if (!token) { 210 continue; 211 } 212 213 const name = auth.name ?? 'Authorization'; 214 215 switch (auth.in) { 216 case 'query': 217 if (!options.query) { 218 options.query = {}; 219 } 220 options.query[name] = token; 221 break; 222 case 'cookie': 223 options.headers.append('Cookie', `${name}=${token}`); 224 break; 225 case 'header': 226 default: 227 options.headers.set(name, token); 228 break; 229 } 230 } 231}; 232 233export const buildUrl: Client['buildUrl'] = (options) => { 234 const url = getUrl({ 235 baseUrl: options.baseUrl as string, 236 path: options.path, 237 query: options.query, 238 querySerializer: 239 typeof options.querySerializer === 'function' 240 ? options.querySerializer 241 : createQuerySerializer(options.querySerializer), 242 url: options.url, 243 }); 244 return url; 245}; 246 247export const getUrl = ({ 248 baseUrl, 249 path, 250 query, 251 querySerializer, 252 url: _url, 253}: { 254 baseUrl?: string; 255 path?: Record<string, unknown>; 256 query?: Record<string, unknown>; 257 querySerializer: QuerySerializer; 258 url: string; 259}) => { 260 const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 261 let url = (baseUrl ?? '') + pathUrl; 262 if (path) { 263 url = defaultPathSerializer({ path, url }); 264 } 265 let search = query ? querySerializer(query) : ''; 266 if (search.startsWith('?')) { 267 search = search.substring(1); 268 } 269 if (search) { 270 url += `?${search}`; 271 } 272 return url; 273}; 274 275export const mergeConfigs = (a: Config, b: Config): Config => { 276 const config = { ...a, ...b }; 277 if (config.baseUrl?.endsWith('/')) { 278 config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 279 } 280 config.headers = mergeHeaders(a.headers, b.headers); 281 return config; 282}; 283 284export const mergeHeaders = ( 285 ...headers: Array<Required<Config>['headers'] | undefined> 286): Headers => { 287 const mergedHeaders = new Headers(); 288 for (const header of headers) { 289 if (!header || typeof header !== 'object') { 290 continue; 291 } 292 293 const iterator = header instanceof Headers ? header.entries() : Object.entries(header); 294 295 for (const [key, value] of iterator) { 296 if (value === null) { 297 mergedHeaders.delete(key); 298 } else if (Array.isArray(value)) { 299 for (const v of value) { 300 mergedHeaders.append(key, v as string); 301 } 302 } else if (value !== undefined) { 303 // assume object headers are meant to be JSON stringified, i.e. their 304 // content value in OpenAPI specification is 'application/json' 305 mergedHeaders.set( 306 key, 307 typeof value === 'object' ? JSON.stringify(value) : (value as string), 308 ); 309 } 310 } 311 } 312 return mergedHeaders; 313}; 314 315type ErrInterceptor<Err, Res, Req, Options> = ( 316 error: Err, 317 response: Res, 318 request: Req, 319 options: Options, 320) => Err | Promise<Err>; 321 322type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>; 323 324type ResInterceptor<Res, Req, Options> = ( 325 response: Res, 326 request: Req, 327 options: Options, 328) => Res | Promise<Res>; 329 330class Interceptors<Interceptor> { 331 fns: Array<Interceptor | null> = []; 332 333 clear(): void { 334 this.fns = []; 335 } 336 337 eject(id: number | Interceptor): void { 338 const index = this.getInterceptorIndex(id); 339 if (this.fns[index]) { 340 this.fns[index] = null; 341 } 342 } 343 344 exists(id: number | Interceptor): boolean { 345 const index = this.getInterceptorIndex(id); 346 return Boolean(this.fns[index]); 347 } 348 349 getInterceptorIndex(id: number | Interceptor): number { 350 if (typeof id === 'number') { 351 return this.fns[id] ? id : -1; 352 } 353 return this.fns.indexOf(id); 354 } 355 356 update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { 357 const index = this.getInterceptorIndex(id); 358 if (this.fns[index]) { 359 this.fns[index] = fn; 360 return id; 361 } 362 return false; 363 } 364 365 use(fn: Interceptor): number { 366 this.fns.push(fn); 367 return this.fns.length - 1; 368 } 369} 370 371export interface Middleware<Req, Res, Err, Options> { 372 error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 373 request: Interceptors<ReqInterceptor<Req, Options>>; 374 response: Interceptors<ResInterceptor<Res, Req, Options>>; 375} 376 377export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 378 Req, 379 Res, 380 Err, 381 Options 382> => ({ 383 error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 384 request: new Interceptors<ReqInterceptor<Req, Options>>(), 385 response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 386}); 387 388const defaultQuerySerializer = createQuerySerializer({ 389 allowReserved: false, 390 array: { 391 explode: true, 392 style: 'form', 393 }, 394 object: { 395 explode: true, 396 style: 'deepObject', 397 }, 398}); 399 400const defaultHeaders = { 401 'Content-Type': 'application/json', 402}; 403 404export const createConfig = <T extends ClientOptions = ClientOptions>( 405 override: Config<Omit<ClientOptions, keyof T> & T> = {}, 406): Config<Omit<ClientOptions, keyof T> & T> => ({ 407 ...jsonBodySerializer, 408 headers: defaultHeaders, 409 parseAs: 'auto', 410 querySerializer: defaultQuerySerializer, 411 ...override, 412});