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

feat: Refactor Angular client integration and enhance error handling

- Updated `openapi-ts.config.ts` to configure Angular client plugin with error handling option.
- Modified `app.component.html` to use a demo component for displaying pet information.
- Refactored `app.component.ts` to remove direct API calls and integrate with the new demo component.
- Enhanced `app.config.ts` to provide the Hey API client for dependency injection.
- Improved error handling in client utilities and response processing.
- Added new demo component with HTML and CSS for displaying pet information and error messages.
- Updated client generation scripts to support new error handling and response structures.
- Refactored HTTP client usage to support custom injectors and improved error response handling.

Max Scopp e50201ff 275f4039

+535 -131
+4 -1
examples/openapi-ts-angular/openapi-ts.config.ts
··· 9 9 path: './src/client', 10 10 }, 11 11 plugins: [ 12 - '@hey-api/client-angular', 12 + { 13 + name: '@hey-api/client-angular', 14 + // throwOnError: true, 15 + }, 13 16 '@hey-api/schemas', 14 17 { 15 18 asClass: false,
+3 -2
examples/openapi-ts-angular/src/app/app.component.html
··· 243 243 <clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath> 244 244 </defs> 245 245 </svg> 246 - <h1>&#64;hey-api/openapi-ts 🤝 Angular</h1> 247 - <button (click)="onGetPetById()" type="button">Get Random Pet</button> 246 + <h1>{{ title }}</h1> 247 + 248 + <app-demo /> 248 249 </div> 249 250 <div class="divider" role="separator" aria-label="Divider"></div> 250 251 <div class="right-side">
+5 -38
examples/openapi-ts-angular/src/app/app.component.ts
··· 1 - import { HttpClient } from '@angular/common/http'; 2 - import { Component, inject } from '@angular/core'; 1 + import { Component } from '@angular/core'; 3 2 import { RouterOutlet } from '@angular/router'; 4 3 5 - import { getPetById } from '../client'; 6 - import { createClient } from '../client/client'; 7 - 8 - const localClient = createClient({ 9 - // set default base url for requests made by this client 10 - baseUrl: 'https://petstore3.swagger.io/api/v3', 11 - /** 12 - * Set default headers only for requests made by this client. This is to 13 - * demonstrate local clients and their configuration taking precedence over 14 - * internal service client. 15 - */ 16 - headers: { 17 - Authorization: 'Bearer <token_from_local_client>', 18 - }, 19 - }); 4 + import { Demo } from './demo/demo'; 20 5 21 6 @Component({ 22 - imports: [RouterOutlet], 7 + host: { ngSkipHydration: 'true' }, 8 + imports: [RouterOutlet, Demo], 23 9 selector: 'app-root', 24 10 styleUrl: './app.component.css', 25 11 templateUrl: './app.component.html', 26 12 }) 27 13 export class AppComponent { 28 - title = 'angular'; 29 - 30 - private http = inject(HttpClient); 31 - 32 - async onGetPetById() { 33 - this.http.get('', {}); 34 - const { data, error } = await getPetById({ 35 - client: localClient, 36 - path: { 37 - // random id 1-10 38 - petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), 39 - }, 40 - }); 41 - if (error) { 42 - console.log(error); 43 - return; 44 - } 45 - console.log(data); 46 - // setPet(data!); 47 - } 14 + title = '@hey-api/openapi-ts 🤝 Angular'; 48 15 }
+3
examples/openapi-ts-angular/src/app/app.config.ts
··· 7 7 } from '@angular/platform-browser'; 8 8 import { provideRouter } from '@angular/router'; 9 9 10 + import { client } from '../client/client.gen'; 11 + import { provideHeyApiClient } from '../client/client/client.gen'; 10 12 import { routes } from './app.routes'; 11 13 12 14 export const appConfig: ApplicationConfig = { ··· 15 17 provideRouter(routes), 16 18 provideClientHydration(withEventReplay()), 17 19 provideHttpClient(withFetch()), 20 + provideHeyApiClient(client), 18 21 ], 19 22 };
+156
examples/openapi-ts-angular/src/app/demo/demo.css
··· 1 + /* Pet Card Styles */ 2 + .pet-card { 3 + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); 4 + border-radius: 16px; 5 + padding: 1.5rem; 6 + margin-top: 1.5rem; 7 + box-shadow: 8 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 9 + 0 2px 4px -1px rgba(0, 0, 0, 0.06); 10 + border: 1px solid rgba(0, 0, 0, 0.05); 11 + transition: 12 + transform 120ms ease, 13 + box-shadow 120ms ease; 14 + } 15 + 16 + .pet-info { 17 + display: flex; 18 + align-items: center; 19 + gap: 1rem; 20 + } 21 + 22 + .pet-avatar { 23 + width: 64px; 24 + height: 64px; 25 + border-radius: 12px; 26 + overflow: hidden; 27 + flex-shrink: 0; 28 + background: var(--red-to-pink-to-purple-horizontal-gradient); 29 + display: flex; 30 + align-items: center; 31 + justify-content: center; 32 + } 33 + 34 + .pet-image { 35 + width: 100%; 36 + height: 100%; 37 + object-fit: cover; 38 + } 39 + 40 + .pet-placeholder { 41 + color: white; 42 + font-size: 1.5rem; 43 + font-weight: 600; 44 + text-transform: uppercase; 45 + } 46 + 47 + .pet-details { 48 + flex: 1; 49 + display: flex; 50 + flex-direction: column; 51 + gap: 0.5rem; 52 + } 53 + 54 + .pet-name { 55 + font-size: 1.125rem; 56 + font-weight: 600; 57 + color: var(--gray-900); 58 + margin: 0; 59 + } 60 + 61 + .pet-category { 62 + font-size: 0.875rem; 63 + color: var(--gray-700); 64 + background: rgba(255, 65, 248, 0.1); 65 + padding: 0.25rem 0.75rem; 66 + border-radius: 20px; 67 + display: inline-block; 68 + width: fit-content; 69 + } 70 + 71 + /* Error Message Styles */ 72 + .error-message { 73 + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); 74 + border: 1px solid #fecaca; 75 + border-radius: 12px; 76 + padding: 1rem; 77 + margin-top: 1.5rem; 78 + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1); 79 + } 80 + 81 + .error-title { 82 + font-size: 1rem; 83 + font-weight: 600; 84 + color: #dc2626; 85 + margin-bottom: 0.5rem; 86 + display: flex; 87 + align-items: center; 88 + gap: 0.5rem; 89 + } 90 + 91 + .error-title::before { 92 + content: '⚠️'; 93 + font-size: 1.125rem; 94 + } 95 + 96 + .error-details { 97 + font-size: 0.875rem; 98 + color: #991b1b; 99 + background: rgba(239, 68, 68, 0.05); 100 + padding: 0.75rem; 101 + border-radius: 8px; 102 + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 103 + white-space: pre-wrap; 104 + word-break: break-word; 105 + border-left: 3px solid #ef4444; 106 + } 107 + 108 + /* Button Styles Enhancement */ 109 + button { 110 + background: var(--red-to-pink-to-purple-horizontal-gradient); 111 + color: white; 112 + border: none; 113 + padding: 0.75rem 1.5rem; 114 + border-radius: 8px; 115 + font-size: 0.875rem; 116 + font-weight: 500; 117 + cursor: pointer; 118 + transition: all 120ms ease; 119 + margin-top: 1rem; 120 + margin-right: 0.5rem; 121 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 122 + } 123 + 124 + button:hover { 125 + transform: translateY(-1px); 126 + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); 127 + } 128 + 129 + button:active { 130 + transform: translateY(0); 131 + } 132 + 133 + /* Responsive adjustments */ 134 + @media screen and (max-width: 650px) { 135 + .pet-card { 136 + padding: 1rem; 137 + margin-top: 1rem; 138 + } 139 + 140 + .pet-info { 141 + flex-direction: column; 142 + text-align: center; 143 + gap: 1rem; 144 + } 145 + 146 + .pet-avatar { 147 + width: 80px; 148 + height: 80px; 149 + align-self: center; 150 + } 151 + 152 + .error-message { 153 + padding: 0.75rem; 154 + margin-top: 1rem; 155 + } 156 + }
+37
examples/openapi-ts-angular/src/app/demo/demo.html
··· 1 + <button (click)="onGetPetById()" type="button">Get Random Pet</button> 2 + <button (click)="onGetPetByIdLocalClient()" type="button"> 3 + Get Random Pet (local client) 4 + </button> 5 + 6 + <!-- Error Display --> 7 + @if (error()) { 8 + <div class="error-message"> 9 + <div class="error-title">Error occurred:</div> 10 + <div class="error-details">{{ error()?.error }}</div> 11 + </div> 12 + } 13 + 14 + <!-- Pet Display Card --> 15 + @if (pet()) { 16 + <div class="pet-card"> 17 + <div class="pet-info"> 18 + <div class="pet-avatar"> 19 + @if (pet()?.photoUrls?.[0]) { 20 + <img 21 + [src]="pet()?.photoUrls?.[0]" 22 + [alt]="pet()?.name || 'Pet'" 23 + class="pet-image" 24 + /> 25 + } @else { 26 + <div class="pet-placeholder">{{ pet()?.name?.slice(0, 1) || 'N' }}</div> 27 + } 28 + </div> 29 + <div class="pet-details"> 30 + <div class="pet-name">Name: {{ pet()?.name || 'N/A' }}</div> 31 + <div class="pet-category"> 32 + Category: {{ pet()?.category?.name || 'N/A' }} 33 + </div> 34 + </div> 35 + </div> 36 + </div> 37 + }
+97
examples/openapi-ts-angular/src/app/demo/demo.ts
··· 1 + import { JsonPipe } from '@angular/common'; 2 + import type { HttpErrorResponse } from '@angular/common/http'; 3 + import { HttpClient } from '@angular/common/http'; 4 + import { Component, inject, signal } from '@angular/core'; 5 + import { RouterOutlet } from '@angular/router'; 6 + 7 + import type { AddPetErrors, Pet } from '../../client'; 8 + import { getPetById } from '../../client'; 9 + import { createClient } from '../../client/client'; 10 + 11 + const localClient = createClient({ 12 + // set default base url for requests made by this client 13 + baseUrl: 'https://petstore3.swagger.io/api/v3', 14 + /** 15 + * Set default headers only for requests made by this client. This is to 16 + * demonstrate local clients and their configuration taking precedence over 17 + * internal service client. 18 + */ 19 + headers: { 20 + Authorization: 'Bearer <token_from_local_client>', 21 + }, 22 + }); 23 + 24 + @Component({ 25 + host: { ngSkipHydration: 'true' }, 26 + imports: [RouterOutlet, JsonPipe], 27 + selector: 'app-demo', 28 + styleUrl: './demo.css', 29 + templateUrl: './demo.html', 30 + }) 31 + export class Demo { 32 + pet = signal<Pet | undefined>(undefined); 33 + error = signal< 34 + | undefined 35 + | { 36 + error: AddPetErrors[keyof AddPetErrors] | Error; 37 + response: HttpErrorResponse; 38 + } 39 + >(undefined); 40 + 41 + #http = inject(HttpClient); 42 + 43 + // // you can set a global httpClient for this client like so, in your app.config, or on each request (like below) 44 + // ngOnInit(): void { 45 + // localClient.setConfig({ 46 + // httpClient: this.#http, 47 + // }); 48 + // } 49 + 50 + onGetPetByIdLocalClient = async () => { 51 + const { data, error, response } = await getPetById({ 52 + client: localClient, 53 + httpClient: this.#http, 54 + path: { 55 + // random id 1-10 56 + petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), 57 + }, 58 + }); 59 + 60 + if (error) { 61 + console.log(error); 62 + this.error.set({ 63 + error, 64 + response: response as HttpErrorResponse, 65 + }); 66 + return; 67 + } 68 + 69 + this.pet.set(data); 70 + this.error.set(undefined); 71 + }; 72 + 73 + onGetPetById = async () => { 74 + const { data, error, response } = await getPetById({ 75 + client: localClient, 76 + httpClient: this.#http, 77 + path: { 78 + // random id 1-10 79 + petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), 80 + }, 81 + }); 82 + 83 + if (error) { 84 + console.log(error); 85 + this.error.set({ 86 + error, 87 + response: response as HttpErrorResponse, 88 + }); 89 + return; 90 + } else { 91 + console.log(error); 92 + console.log(response); 93 + this.pet.set(data); 94 + this.error.set(undefined); 95 + } 96 + }; 97 + }
+37 -15
examples/openapi-ts-angular/src/client/client/client.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 3 import type { HttpResponse } from '@angular/common/http'; 4 - import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; 4 + import { 5 + HttpClient, 6 + HttpErrorResponse, 7 + HttpEventType, 8 + HttpRequest, 9 + } from '@angular/common/http'; 5 10 import { 6 11 assertInInjectionContext, 7 12 inject, 8 13 provideAppInitializer, 14 + runInInjectionContext, 9 15 } from '@angular/core'; 10 16 import { firstValueFrom } from 'rxjs'; 11 17 import { filter } from 'rxjs/operators'; ··· 54 60 }; 55 61 56 62 if (!opts.httpClient) { 57 - assertInInjectionContext(request); 58 - opts.httpClient = inject(HttpClient); 63 + if (opts.injector) { 64 + opts.httpClient = runInInjectionContext(opts.injector, () => 65 + inject(HttpClient), 66 + ); 67 + } else { 68 + assertInInjectionContext(request); 69 + opts.httpClient = inject(HttpClient); 70 + } 59 71 } 60 72 61 73 if (opts.security) { ··· 111 123 } 112 124 } 113 125 114 - let bodyResponse = response.body as Record<string, unknown>; 126 + let bodyResponse: any = response.body; 115 127 116 128 if (opts.responseValidator) { 117 129 await opts.responseValidator(bodyResponse); 118 130 } 119 131 120 132 if (opts.responseTransformer) { 121 - bodyResponse = (await opts.responseTransformer(bodyResponse)) as Record< 122 - string, 123 - unknown 124 - >; 133 + bodyResponse = await opts.responseTransformer(bodyResponse); 125 134 } 126 135 127 - return ( 128 - opts.responseStyle === 'data' 129 - ? bodyResponse 130 - : { data: bodyResponse, ...result } 131 - ) as any; 136 + return opts.responseStyle === 'data' 137 + ? bodyResponse 138 + : { data: bodyResponse, ...result }; 132 139 } catch (error) { 140 + if (error instanceof HttpErrorResponse) { 141 + response = error; 142 + } 143 + 144 + let finalError = error instanceof HttpErrorResponse ? error.error : error; 145 + 133 146 for (const fn of interceptors.error._fns) { 134 147 if (fn) { 135 - (await fn(error, response!, req, opts)) as string; 148 + finalError = (await fn( 149 + finalError, 150 + response as HttpResponse<unknown>, 151 + req, 152 + opts, 153 + )) as string; 136 154 } 137 155 } 138 156 157 + if (opts.throwOnError) { 158 + throw finalError; 159 + } 160 + 139 161 return opts.responseStyle === 'data' 140 162 ? undefined 141 163 : { 142 - error, 164 + error: finalError, 143 165 ...result, 144 166 }; 145 167 }
+43 -11
examples/openapi-ts-angular/src/client/client/types.gen.ts
··· 2 2 3 3 import type { 4 4 HttpClient, 5 + HttpErrorResponse, 6 + HttpHeaders, 5 7 HttpRequest, 6 8 HttpResponse, 7 9 } from '@angular/common/http'; 10 + import type { Injector } from '@angular/core'; 8 11 9 12 import type { Auth } from '../core/auth.gen'; 10 13 import type { ··· 17 20 18 21 export interface Config<T extends ClientOptions = ClientOptions> 19 22 extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 20 - CoreConfig { 23 + Omit<CoreConfig, 'headers'> { 21 24 /** 22 25 * Base URL for all requests made by this client. 23 26 */ 24 27 baseUrl?: T['baseUrl']; 25 28 /** 29 + * An object containing any HTTP headers that you want to pre-populate your 30 + * `HttpHeaders` object with. 31 + * 32 + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} 33 + */ 34 + headers?: 35 + | HttpHeaders 36 + | Record< 37 + string, 38 + | string 39 + | number 40 + | boolean 41 + | (string | number | boolean)[] 42 + | null 43 + | undefined 44 + | unknown 45 + >; 46 + /** 26 47 * The HTTP client to use for making requests. 27 48 */ 28 49 httpClient?: HttpClient; ··· 32 53 * @default 'fields' 33 54 */ 34 55 responseStyle?: ResponseStyle; 56 + 57 + /** 58 + * Throw an error instead of returning it in the response? 59 + * 60 + * @default false 61 + */ 62 + throwOnError?: T['throwOnError']; 35 63 } 36 64 37 65 export interface RequestOptions< ··· 48 76 * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 49 77 */ 50 78 body?: unknown; 79 + /** 80 + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. 81 + */ 82 + injector?: Injector; 51 83 path?: Record<string, unknown>; 52 84 query?: Record<string, unknown>; 53 85 /** ··· 80 112 data: TData extends Record<string, unknown> 81 113 ? TData[keyof TData] 82 114 : TData; 83 - request: Request; 84 - response: Response; 115 + request: HttpRequest<unknown>; 116 + response: HttpResponse<TData>; 85 117 } 86 118 > 87 119 : Promise< ··· 91 123 ? TData[keyof TData] 92 124 : TData) 93 125 | undefined 94 - : ( 126 + : 95 127 | { 96 128 data: TData extends Record<string, unknown> 97 129 ? TData[keyof TData] 98 130 : TData; 99 131 error: undefined; 132 + request: HttpRequest<unknown>; 133 + response: HttpResponse<TData>; 100 134 } 101 135 | { 102 136 data: undefined; 103 - error: TError extends Record<string, unknown> 104 - ? TError[keyof TError] 105 - : TError; 137 + error: TError[keyof TError]; 138 + request: HttpRequest<unknown>; 139 + response: HttpErrorResponse & { 140 + error: TError[keyof TError] | null; 141 + }; 106 142 } 107 - ) & { 108 - request: Request; 109 - response: Response; 110 - } 111 143 >; 112 144 113 145 export interface ClientOptions {
+35 -19
examples/openapi-ts-angular/src/client/client/utils.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 + import { HttpHeaders } from '@angular/common/http'; 4 + 3 5 import { getAuthToken } from '../core/auth.gen'; 4 6 import type { 5 7 QuerySerializer, ··· 198 200 ...options 199 201 }: Pick<Required<RequestOptions>, 'security'> & 200 202 Pick<RequestOptions, 'auth' | 'query'> & { 201 - headers: Headers; 203 + headers: HttpHeaders; 202 204 }) => { 203 205 for (const auth of security) { 204 206 const token = await getAuthToken(auth, options.auth); ··· 282 284 283 285 export const mergeHeaders = ( 284 286 ...headers: Array<Required<Config>['headers'] | undefined> 285 - ): Headers => { 286 - const mergedHeaders = new Headers(); 287 + ): HttpHeaders => { 288 + let mergedHeaders = new HttpHeaders(); 289 + 287 290 for (const header of headers) { 288 291 if (!header || typeof header !== 'object') { 289 292 continue; 290 293 } 291 294 292 - const iterator = 293 - 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); 295 + if (header instanceof HttpHeaders) { 296 + // Merge HttpHeaders instance 297 + header.keys().forEach((key) => { 298 + const values = header.getAll(key); 299 + if (values) { 300 + values.forEach((value) => { 301 + mergedHeaders = mergedHeaders.append(key, value); 302 + }); 301 303 } 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 - ); 304 + }); 305 + } else { 306 + // Merge plain object headers 307 + for (const [key, value] of Object.entries(header)) { 308 + if (value === null) { 309 + mergedHeaders = mergedHeaders.delete(key); 310 + } else if (Array.isArray(value)) { 311 + for (const v of value) { 312 + mergedHeaders = mergedHeaders.append(key, v as string); 313 + } 314 + } else if (value !== undefined) { 315 + // assume object headers are meant to be JSON stringified, i.e. their 316 + // content value in OpenAPI specification is 'application/json' 317 + mergedHeaders = mergedHeaders.set( 318 + key, 319 + typeof value === 'object' 320 + ? JSON.stringify(value) 321 + : (value as string), 322 + ); 323 + } 309 324 } 310 325 } 311 326 } 327 + 312 328 return mergedHeaders; 313 329 }; 314 330
+37 -15
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts
··· 1 1 import type { HttpResponse } from '@angular/common/http'; 2 - import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; 2 + import { 3 + HttpClient, 4 + HttpErrorResponse, 5 + HttpEventType, 6 + HttpRequest, 7 + } from '@angular/common/http'; 3 8 import { 4 9 assertInInjectionContext, 5 10 inject, 6 11 provideAppInitializer, 12 + runInInjectionContext, 7 13 } from '@angular/core'; 8 14 import { firstValueFrom } from 'rxjs'; 9 15 import { filter } from 'rxjs/operators'; ··· 52 58 }; 53 59 54 60 if (!opts.httpClient) { 55 - assertInInjectionContext(request); 56 - opts.httpClient = inject(HttpClient); 61 + if (opts.injector) { 62 + opts.httpClient = runInInjectionContext(opts.injector, () => 63 + inject(HttpClient), 64 + ); 65 + } else { 66 + assertInInjectionContext(request); 67 + opts.httpClient = inject(HttpClient); 68 + } 57 69 } 58 70 59 71 if (opts.security) { ··· 109 121 } 110 122 } 111 123 112 - let bodyResponse = response.body as Record<string, unknown>; 124 + let bodyResponse: any = response.body; 113 125 114 126 if (opts.responseValidator) { 115 127 await opts.responseValidator(bodyResponse); 116 128 } 117 129 118 130 if (opts.responseTransformer) { 119 - bodyResponse = (await opts.responseTransformer(bodyResponse)) as Record< 120 - string, 121 - unknown 122 - >; 131 + bodyResponse = await opts.responseTransformer(bodyResponse); 123 132 } 124 133 125 - return ( 126 - opts.responseStyle === 'data' 127 - ? bodyResponse 128 - : { data: bodyResponse, ...result } 129 - ) as any; 134 + return opts.responseStyle === 'data' 135 + ? bodyResponse 136 + : { data: bodyResponse, ...result }; 130 137 } catch (error) { 138 + if (error instanceof HttpErrorResponse) { 139 + response = error; 140 + } 141 + 142 + let finalError = error instanceof HttpErrorResponse ? error.error : error; 143 + 131 144 for (const fn of interceptors.error._fns) { 132 145 if (fn) { 133 - (await fn(error, response!, req, opts)) as string; 146 + finalError = (await fn( 147 + finalError, 148 + response as HttpResponse<unknown>, 149 + req, 150 + opts, 151 + )) as string; 134 152 } 135 153 } 136 154 155 + if (opts.throwOnError) { 156 + throw finalError; 157 + } 158 + 137 159 return opts.responseStyle === 'data' 138 160 ? undefined 139 161 : { 140 - error, 162 + error: finalError, 141 163 ...result, 142 164 }; 143 165 }
+43 -11
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts
··· 1 1 import type { 2 2 HttpClient, 3 + HttpErrorResponse, 4 + HttpHeaders, 3 5 HttpRequest, 4 6 HttpResponse, 5 7 } from '@angular/common/http'; 8 + import type { Injector } from '@angular/core'; 6 9 7 10 import type { Auth } from '../../client-core/bundle/auth'; 8 11 import type { ··· 15 18 16 19 export interface Config<T extends ClientOptions = ClientOptions> 17 20 extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 18 - CoreConfig { 21 + Omit<CoreConfig, 'headers'> { 19 22 /** 20 23 * Base URL for all requests made by this client. 21 24 */ 22 25 baseUrl?: T['baseUrl']; 23 26 /** 27 + * An object containing any HTTP headers that you want to pre-populate your 28 + * `HttpHeaders` object with. 29 + * 30 + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} 31 + */ 32 + headers?: 33 + | HttpHeaders 34 + | Record< 35 + string, 36 + | string 37 + | number 38 + | boolean 39 + | (string | number | boolean)[] 40 + | null 41 + | undefined 42 + | unknown 43 + >; 44 + /** 24 45 * The HTTP client to use for making requests. 25 46 */ 26 47 httpClient?: HttpClient; ··· 30 51 * @default 'fields' 31 52 */ 32 53 responseStyle?: ResponseStyle; 54 + 55 + /** 56 + * Throw an error instead of returning it in the response? 57 + * 58 + * @default false 59 + */ 60 + throwOnError?: T['throwOnError']; 33 61 } 34 62 35 63 export interface RequestOptions< ··· 46 74 * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 47 75 */ 48 76 body?: unknown; 77 + /** 78 + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. 79 + */ 80 + injector?: Injector; 49 81 path?: Record<string, unknown>; 50 82 query?: Record<string, unknown>; 51 83 /** ··· 78 110 data: TData extends Record<string, unknown> 79 111 ? TData[keyof TData] 80 112 : TData; 81 - request: Request; 82 - response: Response; 113 + request: HttpRequest<unknown>; 114 + response: HttpResponse<TData>; 83 115 } 84 116 > 85 117 : Promise< ··· 89 121 ? TData[keyof TData] 90 122 : TData) 91 123 | undefined 92 - : ( 124 + : 93 125 | { 94 126 data: TData extends Record<string, unknown> 95 127 ? TData[keyof TData] 96 128 : TData; 97 129 error: undefined; 130 + request: HttpRequest<unknown>; 131 + response: HttpResponse<TData>; 98 132 } 99 133 | { 100 134 data: undefined; 101 - error: TError extends Record<string, unknown> 102 - ? TError[keyof TError] 103 - : TError; 135 + error: TError[keyof TError]; 136 + request: HttpRequest<unknown>; 137 + response: HttpErrorResponse & { 138 + error: TError[keyof TError] | null; 139 + }; 104 140 } 105 - ) & { 106 - request: Request; 107 - response: Response; 108 - } 109 141 >; 110 142 111 143 export interface ClientOptions {
+35 -19
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts
··· 1 + import { HttpHeaders } from '@angular/common/http'; 2 + 1 3 import { getAuthToken } from '../../client-core/bundle/auth'; 2 4 import type { 3 5 QuerySerializer, ··· 191 193 ...options 192 194 }: Pick<Required<RequestOptions>, 'security'> & 193 195 Pick<RequestOptions, 'auth' | 'query'> & { 194 - headers: Headers; 196 + headers: HttpHeaders; 195 197 }) => { 196 198 for (const auth of security) { 197 199 const token = await getAuthToken(auth, options.auth); ··· 275 277 276 278 export const mergeHeaders = ( 277 279 ...headers: Array<Required<Config>['headers'] | undefined> 278 - ): Headers => { 279 - const mergedHeaders = new Headers(); 280 + ): HttpHeaders => { 281 + let mergedHeaders = new HttpHeaders(); 282 + 280 283 for (const header of headers) { 281 284 if (!header || typeof header !== 'object') { 282 285 continue; 283 286 } 284 287 285 - const iterator = 286 - header instanceof Headers ? header.entries() : Object.entries(header); 287 - 288 - for (const [key, value] of iterator) { 289 - if (value === null) { 290 - mergedHeaders.delete(key); 291 - } else if (Array.isArray(value)) { 292 - for (const v of value) { 293 - mergedHeaders.append(key, v as string); 288 + if (header instanceof HttpHeaders) { 289 + // Merge HttpHeaders instance 290 + header.keys().forEach((key) => { 291 + const values = header.getAll(key); 292 + if (values) { 293 + values.forEach((value) => { 294 + mergedHeaders = mergedHeaders.append(key, value); 295 + }); 294 296 } 295 - } else if (value !== undefined) { 296 - // assume object headers are meant to be JSON stringified, i.e. their 297 - // content value in OpenAPI specification is 'application/json' 298 - mergedHeaders.set( 299 - key, 300 - typeof value === 'object' ? JSON.stringify(value) : (value as string), 301 - ); 297 + }); 298 + } else { 299 + // Merge plain object headers 300 + for (const [key, value] of Object.entries(header)) { 301 + if (value === null) { 302 + mergedHeaders = mergedHeaders.delete(key); 303 + } else if (Array.isArray(value)) { 304 + for (const v of value) { 305 + mergedHeaders = mergedHeaders.append(key, v as string); 306 + } 307 + } else if (value !== undefined) { 308 + // assume object headers are meant to be JSON stringified, i.e. their 309 + // content value in OpenAPI specification is 'application/json' 310 + mergedHeaders = mergedHeaders.set( 311 + key, 312 + typeof value === 'object' 313 + ? JSON.stringify(value) 314 + : (value as string), 315 + ); 316 + } 302 317 } 303 318 } 304 319 } 320 + 305 321 return mergedHeaders; 306 322 }; 307 323