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

fix: update tanstack example, serializedBody handling and clean up jsonBodySerializer import

Max Scopp 2996bdef 3ad13597

+553 -403
+4 -1
examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts
··· 9 9 path: './src/client', 10 10 }, 11 11 plugins: [ 12 - '@hey-api/client-fetch', 12 + { 13 + name: '@hey-api/client-angular', 14 + // throwOnError: true, 15 + }, 13 16 '@hey-api/schemas', 14 17 '@hey-api/sdk', 15 18 {
+6 -29
examples/openapi-ts-tanstack-angular-query-experimental/src/app/app.config.ts
··· 1 + import { provideHttpClient, withFetch } from '@angular/common/http'; 1 2 import type { ApplicationConfig } from '@angular/core'; 2 3 import { provideZoneChangeDetection } from '@angular/core'; 3 4 import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 4 5 import { provideRouter } from '@angular/router'; 5 6 import { 6 - provideAngularQuery, 7 + provideTanStackQuery, 7 8 QueryClient, 8 9 } from '@tanstack/angular-query-experimental'; 9 10 10 11 import { client } from '../client/client.gen'; 12 + import { provideHeyApiClient } from '../client/client/client.gen'; 11 13 import { routes } from './app.routes'; 12 14 13 - client.setConfig({ 14 - // set default base url for requests made by this client 15 - baseUrl: 'https://petstore3.swagger.io/api/v3', 16 - /** 17 - * Set default headers only for requests made by this client. This is to 18 - * demonstrate local clients and their configuration taking precedence over 19 - * internal service client. 20 - */ 21 - headers: { 22 - Authorization: 'Bearer <token_from_local_client>', 23 - }, 24 - }); 25 - 26 - client.interceptors.request.use((request, options) => { 27 - // Middleware is great for adding authorization tokens to requests made to 28 - // protected paths. Headers are set randomly here to allow surfacing the 29 - // default headers, too. 30 - if ( 31 - options.url === '/pet/{petId}' && 32 - options.method === 'GET' && 33 - Math.random() < 0.5 34 - ) { 35 - request.headers.set('Authorization', 'Bearer <token_from_interceptor>'); 36 - } 37 - return request; 38 - }); 39 - 40 15 export const appConfig: ApplicationConfig = { 41 16 providers: [ 42 17 provideZoneChangeDetection({ eventCoalescing: true }), 43 18 provideRouter(routes), 44 - provideAngularQuery( 19 + provideHttpClient(withFetch()), 20 + provideHeyApiClient(client), 21 + provideTanStackQuery( 45 22 new QueryClient({ 46 23 defaultOptions: { 47 24 queries: {
+50 -2
examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.css
··· 1 1 :host { 2 2 display: grid; 3 3 gap: 20px; 4 - 5 4 max-width: 400px; 6 5 margin: auto; 7 6 } 8 7 8 + .pet-store-header { 9 + display: flex; 10 + gap: 1rem; 11 + align-items: flex-start; 12 + flex-wrap: wrap; 13 + } 14 + 9 15 mat-card { 10 16 margin-bottom: 20px; 11 17 } 12 18 19 + .pet-card { 20 + width: 100%; 21 + margin-top: 1rem; 22 + } 23 + 24 + .pet-card-title { 25 + font-size: 1.4rem; 26 + font-weight: 600; 27 + } 28 + 29 + .pet-card-subtitle { 30 + font-size: 1rem; 31 + color: #666; 32 + } 33 + 34 + .pet-card-image { 35 + object-fit: cover; 36 + min-height: 180px; 37 + background: #fafafa; 38 + } 39 + 40 + .pet-status { 41 + margin-top: 0.5rem; 42 + color: #888; 43 + } 44 + 45 + .pet-form-card { 46 + margin-top: 2rem; 47 + max-width: 400px; 48 + } 49 + 50 + .pet-form-title { 51 + font-size: 1.2rem; 52 + font-weight: 500; 53 + } 54 + 55 + .pet-form-content { 56 + display: flex; 57 + flex-direction: column; 58 + gap: 1rem; 59 + } 60 + 13 61 .actions { 14 62 display: flex; 15 - gap: 10px; 63 + gap: 0.5rem; 16 64 }
+46 -31
examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.html
··· 1 - <mat-card *ngIf="pet.data()!"> 2 - <mat-card-header> 3 - <mat-card-title>{{ pet.data()!.name }}</mat-card-title> 4 - <mat-card-subtitle>{{ pet.data()!.category }}</mat-card-subtitle> 5 - </mat-card-header> 6 - <img 7 - mat-card-image 8 - [src]="pet.data()!.photoUrls.at(0)" 9 - alt="{{ pet.data()!.name }}" 10 - /> 11 - </mat-card> 1 + @let pet = petState.data(); 2 + <div class="pet-store-header"> 3 + <button mat-raised-button color="primary" (click)="getRandomPet()"> 4 + Get Random Pet 5 + </button> 6 + 7 + <mat-card class="pet-card mat-elevation-z8"> 8 + @if (petState.isLoading()) { 9 + <mat-spinner /> 10 + } 12 11 13 - <pre *ngIf="addPet.data()"> 14 - <code>{{addPet.data()|json}}</code> 15 - </pre> 12 + @if (pet) { 13 + <mat-card-header> 14 + <mat-card-title class="pet-card-title">{{ pet.name }}</mat-card-title> 15 + <mat-card-subtitle class="pet-card-subtitle">{{ 16 + pet.category?.name 17 + }}</mat-card-subtitle> 18 + </mat-card-header> 19 + <!-- <img mat-card-image [src]="pet.photoUrls.at(0)" alt="{{ pet.name }}" class="pet-card-image" /> --> 20 + <mat-card-content> 21 + <p class="pet-status"> 22 + Status: <b>{{ pet.status }}</b> 23 + </p> 24 + </mat-card-content> 25 + } 26 + </mat-card> 27 + </div> 16 28 17 - <pre *ngIf="updatePet.data()"> 18 - <code>{{updatePet.data()|json}}</code> 19 - </pre> 29 + @if (addPet.data()) { 30 + <pre> 31 + <code>{{addPet.data()|json}}</code> 32 + </pre> 33 + } 20 34 21 - <button mat-raised-button (click)="getRandomPet()">Get Random Pet</button> 35 + @if (updatePet.data()) { 36 + <pre> 37 + <code>{{updatePet.data()|json}}</code> 38 + </pre> 39 + } 22 40 23 41 <form #petForm="ngForm" (submit)="onSubmit(petForm)"> 24 - <mat-card> 42 + <mat-card class="pet-form-card mat-elevation-z4"> 25 43 <mat-card-header> 26 - <mat-card-title><p>Pet Form</p></mat-card-title> 44 + <mat-card-title 45 + ><span class="pet-form-title">Pet Form</span></mat-card-title 46 + > 27 47 </mat-card-header> 28 - <mat-card-content> 48 + <mat-card-content class="pet-form-content"> 29 49 <mat-form-field appearance="fill"> 30 50 <mat-label>Name</mat-label> 31 - <input matInput name="name" ngModel required /> 32 - </mat-form-field> 33 - 34 - <mat-form-field appearance="fill"> 35 - <mat-label>Category</mat-label> 36 - <input matInput name="category" ngModel required /> 51 + <input matInput name="name" [(ngModel)]="nextPetState.name" required /> 37 52 </mat-form-field> 38 53 </mat-card-content> 39 54 <mat-card-footer> 40 - <mat-card-actions> 41 - <button mat-button color="primary" type="submit">Add Pet</button> 55 + <mat-card-actions class="actions"> 56 + <button mat-raised-button color="primary" type="submit">Add Pet</button> 42 57 <button 43 - mat-button 58 + mat-stroked-button 44 59 color="accent" 45 - (click)="handleUpdatePet(petForm.value.name, petForm.value.category)" 60 + (click)="handleUpdatePet($event)" 46 61 > 47 62 Update Pet 48 63 </button>
+50 -60
examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.ts
··· 1 - import { CommonModule } from '@angular/common'; 2 - import { Component, effect, inject, signal } from '@angular/core'; 3 - import type { NgForm } from '@angular/forms'; 4 - import { FormsModule } from '@angular/forms'; 1 + import { JsonPipe } from '@angular/common'; 2 + import { Component, effect, inject, signal, viewChild } from '@angular/core'; 3 + import { FormsModule, NgForm } from '@angular/forms'; 5 4 import { MatButtonModule } from '@angular/material/button'; 6 5 import { MatCardModule } from '@angular/material/card'; 7 6 import { MatFormFieldModule } from '@angular/material/form-field'; 8 7 import { MatInputModule } from '@angular/material/input'; 8 + import { MatProgressSpinner } from '@angular/material/progress-spinner'; 9 9 import { MatSnackBar } from '@angular/material/snack-bar'; 10 10 import { 11 11 injectMutation, ··· 21 21 22 22 @Component({ 23 23 imports: [ 24 - CommonModule, 24 + JsonPipe, 25 25 FormsModule, 26 26 MatButtonModule, 27 27 MatCardModule, 28 28 MatFormFieldModule, 29 29 MatInputModule, 30 + MatProgressSpinner, 30 31 ], 31 32 selector: 'app-pet-store', 32 33 standalone: true, ··· 35 36 }) 36 37 export class PetStoreComponent { 37 38 #snackbar = inject(MatSnackBar); 39 + form = viewChild.required(NgForm); 38 40 39 - petId = signal<Pet['id']>(null!); 40 - pet = injectQuery(() => ({ 41 - enabled: this.petId() !== null, 41 + petId = signal<Pet['id']>(undefined); 42 + 43 + petState = injectQuery(() => ({ 44 + enabled: (this.petId() ?? 0) > 0, 42 45 ...getPetByIdOptions({ 43 46 path: { petId: this.petId()! }, 44 47 }), 45 48 })); 46 49 47 - addPet = injectMutation(() => addPetMutation()); 48 - updatePet = injectMutation(() => updatePetMutation()); 50 + addPet = injectMutation(() => ({ 51 + ...addPetMutation(), 52 + onError: (err) => { 53 + this.#snackbar.open(err.message); 54 + }, 55 + onSuccess: () => { 56 + this.#snackbar.open('Pet added successfully!'); 57 + }, 58 + })); 59 + updatePet = injectMutation(() => ({ 60 + ...updatePetMutation(), 61 + onError: (err) => { 62 + this.#snackbar.open(err.message); 63 + }, 64 + onSuccess: () => { 65 + this.#snackbar.open('Pet updated successfully!'); 66 + }, 67 + })); 68 + 69 + nextPetState: Partial<Pet> = {}; 49 70 50 71 constructor() { 51 72 effect(() => { 52 - if (this.pet.isError()) { 53 - this.#snackbar.open(`Pet "${this.petId()}" not found.`); 73 + const err = this.petState.error(); 74 + 75 + if (err) { 76 + this.#snackbar.open(String(err)); 54 77 } 55 78 }); 56 - } 57 79 58 - // updatePet = useMutation({ 59 - // ...updatePetMutation(), 60 - // onError: (error) => { 61 - // console.log(error); 62 - // }, 63 - // onSuccess: (data) => { 64 - // setPet(data); 65 - // }, 66 - // }); 67 - 68 - // { data, error } = useQuery({ 69 - // ...getPetByIdOptions({ 70 - // client: localClient, 71 - // path: { 72 - // petId: petId!, 73 - // }, 74 - // }), 75 - // enabled: Boolean(petId), 76 - // }); 80 + effect(() => { 81 + this.nextPetState = { ...this.petState.data() }; 82 + }); 83 + } 77 84 78 85 getRandomPet() { 79 86 // random id 1-10 80 87 this.petId.set(Math.floor(Math.random() * (10 - 1 + 1) + 1)); 81 88 } 82 89 83 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 84 - handleUpdatePet(name: string, category: string) { 85 - throw new Error('Method not implemented.'); 86 - } 90 + handleUpdatePet = async (event: Event) => { 91 + event.preventDefault(); 87 92 88 - onSubmit(form: NgForm) { 93 + await this.updatePet.mutateAsync({ 94 + body: this.nextPetState as Pet, 95 + }); 96 + }; 97 + 98 + onSubmit = async (form: NgForm) => { 89 99 if (!form.valid) { 90 100 return; 91 101 } 92 102 93 - const { category, name } = form.value as { 94 - category: string; 95 - name: string; 96 - }; 97 - 98 - this.addPet.mutate({ 99 - body: { 100 - category: { 101 - id: 0, 102 - name: category, 103 - }, 104 - id: 0, 105 - name, 106 - photoUrls: ['string'], 107 - status: 'available', 108 - tags: [ 109 - { 110 - id: 0, 111 - name: 'string', 112 - }, 113 - ], 114 - }, 103 + await this.addPet.mutateAsync({ 104 + body: this.nextPetState as Pet, 115 105 }); 116 - } 106 + }; 117 107 }
+12 -5
examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts
··· 62 62 Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & { 63 63 _id: string; 64 64 _infinite?: boolean; 65 + tags?: ReadonlyArray<string>; 65 66 }, 66 67 ]; 67 68 ··· 69 70 id: string, 70 71 options?: TOptions, 71 72 infinite?: boolean, 73 + tags?: ReadonlyArray<string>, 72 74 ): [QueryKey<TOptions>[0]] => { 73 75 const params: QueryKey<TOptions>[0] = { 74 76 _id: id, 75 - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, 77 + baseUrl: 78 + options?.baseUrl || 79 + (options?.client ?? _heyApiClient).getConfig().baseUrl, 76 80 } as QueryKey<TOptions>[0]; 77 81 if (infinite) { 78 82 params._infinite = infinite; 83 + } 84 + if (tags) { 85 + params.tags = tags; 79 86 } 80 87 if (options?.body) { 81 88 params.body = options.body; ··· 162 169 }; 163 170 164 171 export const findPetsByStatusQueryKey = ( 165 - options?: Options<FindPetsByStatusData>, 172 + options: Options<FindPetsByStatusData>, 166 173 ) => createQueryKey('findPetsByStatus', options); 167 174 168 175 /** ··· 170 177 * Multiple status values can be provided with comma separated strings. 171 178 */ 172 179 export const findPetsByStatusOptions = ( 173 - options?: Options<FindPetsByStatusData>, 180 + options: Options<FindPetsByStatusData>, 174 181 ) => 175 182 queryOptions({ 176 183 queryFn: async ({ queryKey, signal }) => { ··· 185 192 queryKey: findPetsByStatusQueryKey(options), 186 193 }); 187 194 188 - export const findPetsByTagsQueryKey = (options?: Options<FindPetsByTagsData>) => 195 + export const findPetsByTagsQueryKey = (options: Options<FindPetsByTagsData>) => 189 196 createQueryKey('findPetsByTags', options); 190 197 191 198 /** 192 199 * Finds Pets by tags. 193 200 * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 194 201 */ 195 - export const findPetsByTagsOptions = (options?: Options<FindPetsByTagsData>) => 202 + export const findPetsByTagsOptions = (options: Options<FindPetsByTagsData>) => 196 203 queryOptions({ 197 204 queryFn: async ({ queryKey, signal }) => { 198 205 const { data } = await findPetsByTags({
+190
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { HttpResponse } from '@angular/common/http'; 4 + import { 5 + HttpClient, 6 + HttpErrorResponse, 7 + HttpEventType, 8 + HttpRequest, 9 + } from '@angular/common/http'; 10 + import { 11 + assertInInjectionContext, 12 + inject, 13 + provideAppInitializer, 14 + runInInjectionContext, 15 + } from '@angular/core'; 16 + import { firstValueFrom } from 'rxjs'; 17 + import { filter } from 'rxjs/operators'; 18 + 19 + import type { Client, Config, ResolvedRequestOptions } from './types.gen'; 20 + import { 21 + buildUrl, 22 + createConfig, 23 + createInterceptors, 24 + mergeConfigs, 25 + mergeHeaders, 26 + setAuthParams, 27 + } from './utils.gen'; 28 + 29 + export function provideHeyApiClient(client: Client) { 30 + return provideAppInitializer(() => { 31 + const httpClient = inject(HttpClient); 32 + client.setConfig({ httpClient }); 33 + }); 34 + } 35 + 36 + export const createClient = (config: Config = {}): Client => { 37 + let _config = mergeConfigs(createConfig(), config); 38 + 39 + const getConfig = (): Config => ({ ..._config }); 40 + 41 + const setConfig = (config: Config): Config => { 42 + _config = mergeConfigs(_config, config); 43 + return getConfig(); 44 + }; 45 + 46 + const interceptors = createInterceptors< 47 + HttpRequest<unknown>, 48 + HttpResponse<unknown>, 49 + unknown, 50 + ResolvedRequestOptions 51 + >(); 52 + 53 + const request: Client['request'] = async (options) => { 54 + const opts = { 55 + ..._config, 56 + ...options, 57 + headers: mergeHeaders(_config.headers, options.headers), 58 + httpClient: options.httpClient ?? _config.httpClient, 59 + serializedBody: options.body as any, 60 + }; 61 + 62 + if (!opts.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 + } 71 + } 72 + 73 + if (opts.security) { 74 + await setAuthParams({ 75 + ...opts, 76 + security: opts.security, 77 + }); 78 + } 79 + 80 + if (opts.requestValidator) { 81 + await opts.requestValidator(opts); 82 + } 83 + 84 + if (opts.body && opts.bodySerializer) { 85 + opts.serializedBody = opts.bodySerializer(opts.body); 86 + } 87 + 88 + // remove Content-Type header if body is empty to avoid sending invalid requests 89 + if (opts.serializedBody === undefined || opts.serializedBody === '') { 90 + opts.headers.delete('Content-Type'); 91 + } 92 + 93 + const url = buildUrl(opts); 94 + 95 + let req = new HttpRequest<unknown>( 96 + opts.method, 97 + url, 98 + opts.serializedBody || null, 99 + { 100 + redirect: 'follow', 101 + ...opts, 102 + }, 103 + ); 104 + 105 + for (const fn of interceptors.request._fns) { 106 + if (fn) { 107 + req = await fn(req, opts); 108 + } 109 + } 110 + 111 + let response; 112 + const result = { 113 + request: req, 114 + response, 115 + }; 116 + 117 + try { 118 + response = await firstValueFrom( 119 + opts.httpClient 120 + .request(req) 121 + .pipe(filter((event) => event.type === HttpEventType.Response)), 122 + ); 123 + 124 + for (const fn of interceptors.response._fns) { 125 + if (fn) { 126 + response = await fn(response, req, opts); 127 + } 128 + } 129 + 130 + let bodyResponse: any = response.body; 131 + 132 + if (opts.responseValidator) { 133 + await opts.responseValidator(bodyResponse); 134 + } 135 + 136 + if (opts.responseTransformer) { 137 + bodyResponse = await opts.responseTransformer(bodyResponse); 138 + } 139 + 140 + return opts.responseStyle === 'data' 141 + ? bodyResponse 142 + : { data: bodyResponse, ...result }; 143 + } catch (error) { 144 + if (error instanceof HttpErrorResponse) { 145 + response = error; 146 + } 147 + 148 + let finalError = error instanceof HttpErrorResponse ? error.error : error; 149 + 150 + for (const fn of interceptors.error._fns) { 151 + if (fn) { 152 + finalError = (await fn( 153 + finalError, 154 + response as HttpResponse<unknown>, 155 + req, 156 + opts, 157 + )) as string; 158 + } 159 + } 160 + 161 + if (opts.throwOnError) { 162 + throw finalError; 163 + } 164 + 165 + return opts.responseStyle === 'data' 166 + ? undefined 167 + : { 168 + error: finalError, 169 + ...result, 170 + }; 171 + } 172 + }; 173 + 174 + return { 175 + buildUrl, 176 + connect: (options) => request({ ...options, method: 'CONNECT' }), 177 + delete: (options) => request({ ...options, method: 'DELETE' }), 178 + get: (options) => request({ ...options, method: 'GET' }), 179 + getConfig, 180 + head: (options) => request({ ...options, method: 'HEAD' }), 181 + interceptors, 182 + options: (options) => request({ ...options, method: 'OPTIONS' }), 183 + patch: (options) => request({ ...options, method: 'PATCH' }), 184 + post: (options) => request({ ...options, method: 'POST' }), 185 + put: (options) => request({ ...options, method: 'PUT' }), 186 + request, 187 + setConfig, 188 + trace: (options) => request({ ...options, method: 'TRACE' }), 189 + }; 190 + };
-181
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.ts
··· 1 - import type { Client, Config, RequestOptions } from './types'; 2 - import { 3 - buildUrl, 4 - createConfig, 5 - createInterceptors, 6 - getParseAs, 7 - mergeConfigs, 8 - mergeHeaders, 9 - setAuthParams, 10 - } from './utils'; 11 - 12 - type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 - body?: any; 14 - headers: ReturnType<typeof mergeHeaders>; 15 - }; 16 - 17 - export const createClient = (config: Config = {}): Client => { 18 - let _config = mergeConfigs(createConfig(), config); 19 - 20 - const getConfig = (): Config => ({ ..._config }); 21 - 22 - const setConfig = (config: Config): Config => { 23 - _config = mergeConfigs(_config, config); 24 - return getConfig(); 25 - }; 26 - 27 - const interceptors = createInterceptors< 28 - Request, 29 - Response, 30 - unknown, 31 - RequestOptions 32 - >(); 33 - 34 - const request: Client['request'] = async (options) => { 35 - const opts = { 36 - ..._config, 37 - ...options, 38 - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 - headers: mergeHeaders(_config.headers, options.headers), 40 - }; 41 - 42 - if (opts.security) { 43 - await setAuthParams({ 44 - ...opts, 45 - security: opts.security, 46 - }); 47 - } 48 - 49 - if (opts.body && opts.bodySerializer) { 50 - opts.body = opts.bodySerializer(opts.body); 51 - } 52 - 53 - // remove Content-Type header if body is empty to avoid sending invalid requests 54 - if (opts.body === undefined || opts.body === '') { 55 - opts.headers.delete('Content-Type'); 56 - } 57 - 58 - const url = buildUrl(opts); 59 - const requestInit: ReqInit = { 60 - redirect: 'follow', 61 - ...opts, 62 - }; 63 - 64 - let request = new Request(url, requestInit); 65 - 66 - for (const fn of interceptors.request._fns) { 67 - if (fn) { 68 - request = await fn(request, opts); 69 - } 70 - } 71 - 72 - // fetch must be assigned here, otherwise it would throw the error: 73 - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 - const _fetch = opts.fetch!; 75 - let response = await _fetch(request); 76 - 77 - for (const fn of interceptors.response._fns) { 78 - if (fn) { 79 - response = await fn(response, request, opts); 80 - } 81 - } 82 - 83 - const result = { 84 - request, 85 - response, 86 - }; 87 - 88 - if (response.ok) { 89 - if ( 90 - response.status === 204 || 91 - response.headers.get('Content-Length') === '0' 92 - ) { 93 - return opts.responseStyle === 'data' 94 - ? {} 95 - : { 96 - data: {}, 97 - ...result, 98 - }; 99 - } 100 - 101 - const parseAs = 102 - (opts.parseAs === 'auto' 103 - ? getParseAs(response.headers.get('Content-Type')) 104 - : opts.parseAs) ?? 'json'; 105 - 106 - if (parseAs === 'stream') { 107 - return opts.responseStyle === 'data' 108 - ? response.body 109 - : { 110 - data: response.body, 111 - ...result, 112 - }; 113 - } 114 - 115 - let data = await response[parseAs](); 116 - if (parseAs === 'json') { 117 - if (opts.responseValidator) { 118 - await opts.responseValidator(data); 119 - } 120 - 121 - if (opts.responseTransformer) { 122 - data = await opts.responseTransformer(data); 123 - } 124 - } 125 - 126 - return opts.responseStyle === 'data' 127 - ? data 128 - : { 129 - data, 130 - ...result, 131 - }; 132 - } 133 - 134 - let error = await response.text(); 135 - 136 - try { 137 - error = JSON.parse(error); 138 - } catch { 139 - // noop 140 - } 141 - 142 - let finalError = error; 143 - 144 - for (const fn of interceptors.error._fns) { 145 - if (fn) { 146 - finalError = (await fn(error, response, request, opts)) as string; 147 - } 148 - } 149 - 150 - finalError = finalError || ({} as string); 151 - 152 - if (opts.throwOnError) { 153 - throw finalError; 154 - } 155 - 156 - // TODO: we probably want to return error and improve types 157 - return opts.responseStyle === 'data' 158 - ? undefined 159 - : { 160 - error: finalError, 161 - ...result, 162 - }; 163 - }; 164 - 165 - return { 166 - buildUrl, 167 - connect: (options) => request({ ...options, method: 'CONNECT' }), 168 - delete: (options) => request({ ...options, method: 'DELETE' }), 169 - get: (options) => request({ ...options, method: 'GET' }), 170 - getConfig, 171 - head: (options) => request({ ...options, method: 'HEAD' }), 172 - interceptors, 173 - options: (options) => request({ ...options, method: 'OPTIONS' }), 174 - patch: (options) => request({ ...options, method: 'PATCH' }), 175 - post: (options) => request({ ...options, method: 'POST' }), 176 - put: (options) => request({ ...options, method: 'PUT' }), 177 - request, 178 - setConfig, 179 - trace: (options) => request({ ...options, method: 'TRACE' }), 180 - }; 181 - };
+10 -7
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts
··· 1 - export type { Auth } from '../core/auth'; 2 - export type { QuerySerializerOptions } from '../core/bodySerializer'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 3 5 export { 4 6 formDataBodySerializer, 5 7 jsonBodySerializer, 6 8 urlSearchParamsBodySerializer, 7 - } from '../core/bodySerializer'; 8 - export { buildClientParams } from '../core/params'; 9 - export { createClient } from './client'; 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { createClient } from './client.gen'; 10 12 export type { 11 13 Client, 12 14 ClientOptions, ··· 16 18 OptionsLegacyParser, 17 19 RequestOptions, 18 20 RequestResult, 21 + ResolvedRequestOptions, 19 22 ResponseStyle, 20 23 TDataShape, 21 - } from './types'; 22 - export { createConfig, mergeHeaders } from './utils'; 24 + } from './types.gen'; 25 + export { createConfig, mergeHeaders } from './utils.gen';
+64 -33
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts
··· 1 - import type { Auth } from '../core/auth'; 2 - import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 - import type { Middleware } from './utils'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { 4 + HttpClient, 5 + HttpErrorResponse, 6 + HttpHeaders, 7 + HttpRequest, 8 + HttpResponse, 9 + } from '@angular/common/http'; 10 + import type { Injector } from '@angular/core'; 11 + 12 + import type { Auth } from '../core/auth.gen'; 13 + import type { 14 + Client as CoreClient, 15 + Config as CoreConfig, 16 + } from '../core/types.gen'; 17 + import type { Middleware } from './utils.gen'; 4 18 5 19 export type ResponseStyle = 'data' | 'fields'; 6 20 7 21 export interface Config<T extends ClientOptions = ClientOptions> 8 22 extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 - CoreConfig { 23 + Omit<CoreConfig, 'headers'> { 10 24 /** 11 25 * Base URL for all requests made by this client. 12 26 */ 13 27 baseUrl?: T['baseUrl']; 14 28 /** 15 - * Fetch API implementation. You can use this option to provide a custom 16 - * fetch instance. 29 + * An object containing any HTTP headers that you want to pre-populate your 30 + * `HttpHeaders` object with. 17 31 * 18 - * @default globalThis.fetch 32 + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} 19 33 */ 20 - fetch?: (request: Request) => ReturnType<typeof fetch>; 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 + >; 21 46 /** 22 - * Please don't use the Fetch client for Next.js applications. The `next` 23 - * options won't have any effect. 24 - * 25 - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 47 + * The HTTP client to use for making requests. 26 48 */ 27 - next?: never; 28 - /** 29 - * Return the response data parsed in a specified format. By default, `auto` 30 - * will infer the appropriate method from the `Content-Type` response header. 31 - * You can override this behavior with any of the {@link Body} methods. 32 - * Select `stream` if you don't want to parse response data at all. 33 - * 34 - * @default 'auto' 35 - */ 36 - parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 49 + httpClient?: HttpClient; 37 50 /** 38 51 * Should we return only data or multiple fields (data, error, response, etc.)? 39 52 * 40 53 * @default 'fields' 41 54 */ 42 55 responseStyle?: ResponseStyle; 56 + 43 57 /** 44 58 * Throw an error instead of returning it in the response? 45 59 * ··· 62 76 * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 77 */ 64 78 body?: unknown; 79 + /** 80 + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. 81 + */ 82 + injector?: Injector; 65 83 path?: Record<string, unknown>; 66 84 query?: Record<string, unknown>; 67 85 /** ··· 69 87 */ 70 88 security?: ReadonlyArray<Auth>; 71 89 url: Url; 90 + } 91 + 92 + export interface ResolvedRequestOptions< 93 + TResponseStyle extends ResponseStyle = 'fields', 94 + ThrowOnError extends boolean = boolean, 95 + Url extends string = string, 96 + > extends RequestOptions<TResponseStyle, ThrowOnError, Url> { 97 + serializedBody?: string; 72 98 } 73 99 74 100 export type RequestResult< ··· 86 112 data: TData extends Record<string, unknown> 87 113 ? TData[keyof TData] 88 114 : TData; 89 - request: Request; 90 - response: Response; 115 + request: HttpRequest<unknown>; 116 + response: HttpResponse<TData>; 91 117 } 92 118 > 93 119 : Promise< ··· 97 123 ? TData[keyof TData] 98 124 : TData) 99 125 | undefined 100 - : ( 126 + : 101 127 | { 102 128 data: TData extends Record<string, unknown> 103 129 ? TData[keyof TData] 104 130 : TData; 105 131 error: undefined; 132 + request: HttpRequest<unknown>; 133 + response: HttpResponse<TData>; 106 134 } 107 135 | { 108 136 data: undefined; 109 - error: TError extends Record<string, unknown> 110 - ? TError[keyof TError] 111 - : TError; 137 + error: TError[keyof TError]; 138 + request: HttpRequest<unknown>; 139 + response: HttpErrorResponse & { 140 + error: TError[keyof TError] | null; 141 + }; 112 142 } 113 - ) & { 114 - request: Request; 115 - response: Response; 116 - } 117 143 >; 118 144 119 145 export interface ClientOptions { ··· 153 179 ) => string; 154 180 155 181 export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 - interceptors: Middleware<Request, Response, unknown, RequestOptions>; 182 + interceptors: Middleware< 183 + HttpRequest<unknown>, 184 + HttpResponse<unknown>, 185 + unknown, 186 + ResolvedRequestOptions 187 + >; 157 188 }; 158 189 159 190 /**
+49 -27
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts
··· 1 - import { getAuthToken } from '../core/auth'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { HttpHeaders } from '@angular/common/http'; 4 + 5 + import { getAuthToken } from '../core/auth.gen'; 2 6 import type { 3 7 QuerySerializer, 4 8 QuerySerializerOptions, 5 - } from '../core/bodySerializer'; 6 - import { jsonBodySerializer } from '../core/bodySerializer'; 9 + } from '../core/bodySerializer.gen'; 7 10 import { 8 11 serializeArrayParam, 9 12 serializeObjectParam, 10 13 serializePrimitiveParam, 11 - } from '../core/pathSerializer'; 12 - import type { Client, ClientOptions, Config, RequestOptions } from './types'; 14 + } from '../core/pathSerializer.gen'; 15 + import type { 16 + Client, 17 + ClientOptions, 18 + Config, 19 + RequestOptions, 20 + } from './types.gen'; 13 21 14 22 interface PathSerializer { 15 23 path: Record<string, unknown>; ··· 147 155 */ 148 156 export const getParseAs = ( 149 157 contentType: string | null, 150 - ): Exclude<Config['parseAs'], 'auto'> => { 158 + ): 'blob' | 'formData' | 'json' | 'stream' | 'text' | undefined => { 151 159 if (!contentType) { 152 160 // If no Content-Type header is provided, the best we can do is return the raw response body, 153 161 // which is effectively the same as the 'stream' option. ··· 182 190 if (cleanContent.startsWith('text/')) { 183 191 return 'text'; 184 192 } 193 + 194 + return; 185 195 }; 186 196 187 197 export const setAuthParams = async ({ ··· 189 199 ...options 190 200 }: Pick<Required<RequestOptions>, 'security'> & 191 201 Pick<RequestOptions, 'auth' | 'query'> & { 192 - headers: Headers; 202 + headers: HttpHeaders; 193 203 }) => { 194 204 for (const auth of security) { 195 205 const token = await getAuthToken(auth, options.auth); ··· 273 283 274 284 export const mergeHeaders = ( 275 285 ...headers: Array<Required<Config>['headers'] | undefined> 276 - ): Headers => { 277 - const mergedHeaders = new Headers(); 286 + ): HttpHeaders => { 287 + let mergedHeaders = new HttpHeaders(); 288 + 278 289 for (const header of headers) { 279 290 if (!header || typeof header !== 'object') { 280 291 continue; 281 292 } 282 293 283 - const iterator = 284 - header instanceof Headers ? header.entries() : Object.entries(header); 285 - 286 - for (const [key, value] of iterator) { 287 - if (value === null) { 288 - mergedHeaders.delete(key); 289 - } else if (Array.isArray(value)) { 290 - for (const v of value) { 291 - mergedHeaders.append(key, v as string); 294 + if (header instanceof HttpHeaders) { 295 + // Merge HttpHeaders instance 296 + header.keys().forEach((key) => { 297 + const values = header.getAll(key); 298 + if (values) { 299 + values.forEach((value) => { 300 + mergedHeaders = mergedHeaders.append(key, value); 301 + }); 292 302 } 293 - } else if (value !== undefined) { 294 - // assume object headers are meant to be JSON stringified, i.e. their 295 - // content value in OpenAPI specification is 'application/json' 296 - mergedHeaders.set( 297 - key, 298 - typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 - ); 303 + }); 304 + } else { 305 + // Merge plain object headers 306 + for (const [key, value] of Object.entries(header)) { 307 + if (value === null) { 308 + mergedHeaders = mergedHeaders.delete(key); 309 + } else if (Array.isArray(value)) { 310 + for (const v of value) { 311 + mergedHeaders = mergedHeaders.append(key, v as string); 312 + } 313 + } else if (value !== undefined) { 314 + // assume object headers are meant to be JSON stringified, i.e. their 315 + // content value in OpenAPI specification is 'application/json' 316 + mergedHeaders = mergedHeaders.set( 317 + key, 318 + typeof value === 'object' 319 + ? JSON.stringify(value) 320 + : (value as string), 321 + ); 322 + } 300 323 } 301 324 } 302 325 } 326 + 303 327 return mergedHeaders; 304 328 }; 305 329 ··· 407 431 export const createConfig = <T extends ClientOptions = ClientOptions>( 408 432 override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 433 ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 - ...jsonBodySerializer, 411 434 headers: defaultHeaders, 412 - parseAs: 'auto', 413 435 querySerializer: defaultQuerySerializer, 414 436 ...override, 415 437 });
+2
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 export type AuthToken = string | undefined; 2 4 3 5 export interface Auth {
+13 -7
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 import type { 2 4 ArrayStyle, 3 5 ObjectStyle, 4 6 SerializerOptions, 5 - } from './pathSerializer'; 7 + } from './pathSerializer.gen'; 6 8 7 9 export type QuerySerializer = (query: Record<string, unknown>) => string; 8 10 ··· 14 16 object?: SerializerOptions<ObjectStyle>; 15 17 } 16 18 17 - const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 19 + const serializeFormDataPair = ( 20 + data: FormData, 21 + key: string, 22 + value: unknown, 23 + ): void => { 18 24 if (typeof value === 'string' || value instanceof Blob) { 19 25 data.append(key, value); 20 26 } else { ··· 26 32 data: URLSearchParams, 27 33 key: string, 28 34 value: unknown, 29 - ) => { 35 + ): void => { 30 36 if (typeof value === 'string') { 31 37 data.append(key, value); 32 38 } else { ··· 37 43 export const formDataBodySerializer = { 38 44 bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 45 body: T, 40 - ) => { 46 + ): FormData => { 41 47 const data = new FormData(); 42 48 43 49 Object.entries(body).forEach(([key, value]) => { ··· 56 62 }; 57 63 58 64 export const jsonBodySerializer = { 59 - bodySerializer: <T>(body: T) => 60 - JSON.stringify(body, (key, value) => 65 + bodySerializer: <T>(body: T): string => 66 + JSON.stringify(body, (_key, value) => 61 67 typeof value === 'bigint' ? value.toString() : value, 62 68 ), 63 69 }; ··· 65 71 export const urlSearchParamsBodySerializer = { 66 72 bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 73 body: T, 68 - ) => { 74 + ): string => { 69 75 const data = new URLSearchParams(); 70 76 71 77 Object.entries(body).forEach(([key, value]) => {
+12
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 type Slot = 'body' | 'headers' | 'path' | 'query'; 2 4 3 5 export type Field = 4 6 | { 5 7 in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 6 11 key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 7 16 map?: string; 8 17 } 9 18 | { 10 19 in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 11 23 key?: string; 12 24 map?: string; 13 25 };
+2
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 interface SerializeOptions<T> 2 4 extends SerializePrimitiveOptions, 3 5 SerializerOptions<T> {}
+24 -2
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.ts examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts
··· 1 - import type { Auth, AuthToken } from './auth'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 2 4 import type { 3 5 BodySerializer, 4 6 QuerySerializer, 5 7 QuerySerializerOptions, 6 - } from './bodySerializer'; 8 + } from './bodySerializer.gen'; 7 9 8 10 export interface Client< 9 11 RequestFn = never, ··· 85 87 */ 86 88 querySerializer?: QuerySerializer | QuerySerializerOptions; 87 89 /** 90 + * A function validating request data. This is useful if you want to ensure 91 + * the request conforms to the desired shape, so it can be safely sent to 92 + * the server. 93 + */ 94 + requestValidator?: (data: unknown) => Promise<unknown>; 95 + /** 88 96 * A function transforming response data before it's returned. This is useful 89 97 * for post-processing data, e.g. converting ISO strings into Date objects. 90 98 */ ··· 96 104 */ 97 105 responseValidator?: (data: unknown) => Promise<unknown>; 98 106 } 107 + 108 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 109 + ? true 110 + : [T] extends [never | undefined] 111 + ? [undefined] extends [T] 112 + ? false 113 + : true 114 + : false; 115 + 116 + export type OmitNever<T extends Record<string, unknown>> = { 117 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true 118 + ? never 119 + : K]: T[K]; 120 + };
+4 -4
examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts
··· 136 136 * Multiple status values can be provided with comma separated strings. 137 137 */ 138 138 export const findPetsByStatus = <ThrowOnError extends boolean = false>( 139 - options?: Options<FindPetsByStatusData, ThrowOnError>, 139 + options: Options<FindPetsByStatusData, ThrowOnError>, 140 140 ) => 141 - (options?.client ?? _heyApiClient).get< 141 + (options.client ?? _heyApiClient).get< 142 142 FindPetsByStatusResponses, 143 143 FindPetsByStatusErrors, 144 144 ThrowOnError ··· 158 158 * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 159 159 */ 160 160 export const findPetsByTags = <ThrowOnError extends boolean = false>( 161 - options?: Options<FindPetsByTagsData, ThrowOnError>, 161 + options: Options<FindPetsByTagsData, ThrowOnError>, 162 162 ) => 163 - (options?.client ?? _heyApiClient).get< 163 + (options.client ?? _heyApiClient).get< 164 164 FindPetsByTagsResponses, 165 165 FindPetsByTagsErrors, 166 166 ThrowOnError
+5 -5
examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts
··· 136 136 export type FindPetsByStatusData = { 137 137 body?: never; 138 138 path?: never; 139 - query?: { 139 + query: { 140 140 /** 141 141 * Status values that need to be considered for filter 142 142 */ 143 - status?: 'available' | 'pending' | 'sold'; 143 + status: 'available' | 'pending' | 'sold'; 144 144 }; 145 145 url: '/pet/findByStatus'; 146 146 }; ··· 169 169 export type FindPetsByTagsData = { 170 170 body?: never; 171 171 path?: never; 172 - query?: { 172 + query: { 173 173 /** 174 174 * Tags to filter by 175 175 */ 176 - tags?: Array<string>; 176 + tags: Array<string>; 177 177 }; 178 178 url: '/pet/findByTags'; 179 179 }; ··· 560 560 /** 561 561 * successful operation 562 562 */ 563 - 200: Blob | File; 563 + 200: string; 564 564 }; 565 565 566 566 export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses];
+10 -6
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts
··· 54 54 ...options, 55 55 headers: mergeHeaders(_config.headers, options.headers), 56 56 httpClient: options.httpClient ?? _config.httpClient, 57 - serializedBody: undefined, 57 + serializedBody: options.body as any, 58 58 }; 59 59 60 60 if (!opts.httpClient) { ··· 90 90 91 91 const url = buildUrl(opts); 92 92 93 - let req = new HttpRequest<unknown>(opts.method, url, { 94 - redirect: 'follow', 95 - ...opts, 96 - body: opts.serializedBody, 97 - }); 93 + let req = new HttpRequest<unknown>( 94 + opts.method, 95 + url, 96 + opts.serializedBody || null, 97 + { 98 + redirect: 'follow', 99 + ...opts, 100 + }, 101 + ); 98 102 99 103 for (const fn of interceptors.request._fns) { 100 104 if (fn) {
-3
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts
··· 5 5 QuerySerializer, 6 6 QuerySerializerOptions, 7 7 } from '../../client-core/bundle/bodySerializer'; 8 - import { jsonBodySerializer } from '../../client-core/bundle/bodySerializer'; 9 8 import { 10 9 serializeArrayParam, 11 10 serializeObjectParam, ··· 425 424 export const createConfig = <T extends ClientOptions = ClientOptions>( 426 425 override: Config<Omit<ClientOptions, keyof T> & T> = {}, 427 426 ): Config<Omit<ClientOptions, keyof T> & T> => ({ 428 - ...jsonBodySerializer, 429 427 headers: defaultHeaders, 430 - // parseAs: 'auto', 431 428 querySerializer: defaultQuerySerializer, 432 429 ...override, 433 430 });