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

fix(angular): allow `httpResource` to skip requests when `undefined` is returned

Max Scopp e9770dfd b907a3e4

+1208 -218
+97 -60
examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts
··· 70 70 * Add a new pet to the store. 71 71 */ 72 72 public addPet<ThrowOnError extends boolean = false>( 73 - options: () => Options<AddPetData, ThrowOnError>, 73 + options: () => Options<AddPetData, ThrowOnError> | undefined, 74 74 ) { 75 - return httpResource<AddPetResponse>(() => addPetRequest(options())); 75 + return httpResource<AddPetResponse>(() => { 76 + const opts = options ? options() : undefined; 77 + return opts ? addPetRequest(opts) : undefined; 78 + }); 76 79 } 77 80 78 81 /** ··· 80 83 * Update an existing pet by Id. 81 84 */ 82 85 public updatePet<ThrowOnError extends boolean = false>( 83 - options: () => Options<UpdatePetData, ThrowOnError>, 86 + options: () => Options<UpdatePetData, ThrowOnError> | undefined, 84 87 ) { 85 - return httpResource<UpdatePetResponse>(() => updatePetRequest(options())); 88 + return httpResource<UpdatePetResponse>(() => { 89 + const opts = options ? options() : undefined; 90 + return opts ? updatePetRequest(opts) : undefined; 91 + }); 86 92 } 87 93 88 94 /** ··· 90 96 * Multiple status values can be provided with comma separated strings. 91 97 */ 92 98 public findPetsByStatus<ThrowOnError extends boolean = false>( 93 - options: () => Options<FindPetsByStatusData, ThrowOnError>, 99 + options: () => Options<FindPetsByStatusData, ThrowOnError> | undefined, 94 100 ) { 95 - return httpResource<FindPetsByStatusResponse>(() => 96 - findPetsByStatusRequest(options()), 97 - ); 101 + return httpResource<FindPetsByStatusResponse>(() => { 102 + const opts = options ? options() : undefined; 103 + return opts ? findPetsByStatusRequest(opts) : undefined; 104 + }); 98 105 } 99 106 100 107 /** ··· 102 109 * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 103 110 */ 104 111 public findPetsByTags<ThrowOnError extends boolean = false>( 105 - options: () => Options<FindPetsByTagsData, ThrowOnError>, 112 + options: () => Options<FindPetsByTagsData, ThrowOnError> | undefined, 106 113 ) { 107 - return httpResource<FindPetsByTagsResponse>(() => 108 - findPetsByTagsRequest(options()), 109 - ); 114 + return httpResource<FindPetsByTagsResponse>(() => { 115 + const opts = options ? options() : undefined; 116 + return opts ? findPetsByTagsRequest(opts) : undefined; 117 + }); 110 118 } 111 119 112 120 /** ··· 114 122 * Delete a pet. 115 123 */ 116 124 public deletePet<ThrowOnError extends boolean = false>( 117 - options: () => Options<DeletePetData, ThrowOnError>, 125 + options: () => Options<DeletePetData, ThrowOnError> | undefined, 118 126 ) { 119 - return httpResource<unknown>(() => deletePetRequest(options())); 127 + return httpResource<unknown>(() => { 128 + const opts = options ? options() : undefined; 129 + return opts ? deletePetRequest(opts) : undefined; 130 + }); 120 131 } 121 132 122 133 /** ··· 124 135 * Returns a single pet. 125 136 */ 126 137 public getPetById<ThrowOnError extends boolean = false>( 127 - options: () => Options<GetPetByIdData, ThrowOnError>, 138 + options: () => Options<GetPetByIdData, ThrowOnError> | undefined, 128 139 ) { 129 - return httpResource<GetPetByIdResponse>(() => getPetByIdRequest(options())); 140 + return httpResource<GetPetByIdResponse>(() => { 141 + const opts = options ? options() : undefined; 142 + return opts ? getPetByIdRequest(opts) : undefined; 143 + }); 130 144 } 131 145 132 146 /** ··· 134 148 * Updates a pet resource based on the form data. 135 149 */ 136 150 public updatePetWithForm<ThrowOnError extends boolean = false>( 137 - options: () => Options<UpdatePetWithFormData, ThrowOnError>, 151 + options: () => Options<UpdatePetWithFormData, ThrowOnError> | undefined, 138 152 ) { 139 - return httpResource<UpdatePetWithFormResponse>(() => 140 - updatePetWithFormRequest(options()), 141 - ); 153 + return httpResource<UpdatePetWithFormResponse>(() => { 154 + const opts = options ? options() : undefined; 155 + return opts ? updatePetWithFormRequest(opts) : undefined; 156 + }); 142 157 } 143 158 144 159 /** ··· 146 161 * Upload image of the pet. 147 162 */ 148 163 public uploadFile<ThrowOnError extends boolean = false>( 149 - options: () => Options<UploadFileData, ThrowOnError>, 164 + options: () => Options<UploadFileData, ThrowOnError> | undefined, 150 165 ) { 151 - return httpResource<UploadFileResponse>(() => uploadFileRequest(options())); 166 + return httpResource<UploadFileResponse>(() => { 167 + const opts = options ? options() : undefined; 168 + return opts ? uploadFileRequest(opts) : undefined; 169 + }); 152 170 } 153 171 } 154 172 ··· 161 179 * Returns a map of status codes to quantities. 162 180 */ 163 181 public getInventory<ThrowOnError extends boolean = false>( 164 - options?: () => Options<GetInventoryData, ThrowOnError>, 182 + options?: () => Options<GetInventoryData, ThrowOnError> | undefined, 165 183 ) { 166 - return httpResource<GetInventoryResponse>(() => 167 - getInventoryRequest(options ? options() : undefined), 168 - ); 184 + return httpResource<GetInventoryResponse>(() => { 185 + const opts = options ? options() : undefined; 186 + return opts ? getInventoryRequest(opts) : undefined; 187 + }); 169 188 } 170 189 171 190 /** ··· 173 192 * Place a new order in the store. 174 193 */ 175 194 public placeOrder<ThrowOnError extends boolean = false>( 176 - options?: () => Options<PlaceOrderData, ThrowOnError>, 195 + options?: () => Options<PlaceOrderData, ThrowOnError> | undefined, 177 196 ) { 178 - return httpResource<PlaceOrderResponse>(() => 179 - placeOrderRequest(options ? options() : undefined), 180 - ); 197 + return httpResource<PlaceOrderResponse>(() => { 198 + const opts = options ? options() : undefined; 199 + return opts ? placeOrderRequest(opts) : undefined; 200 + }); 181 201 } 182 202 183 203 /** ··· 185 205 * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. 186 206 */ 187 207 public deleteOrder<ThrowOnError extends boolean = false>( 188 - options: () => Options<DeleteOrderData, ThrowOnError>, 208 + options: () => Options<DeleteOrderData, ThrowOnError> | undefined, 189 209 ) { 190 - return httpResource<unknown>(() => deleteOrderRequest(options())); 210 + return httpResource<unknown>(() => { 211 + const opts = options ? options() : undefined; 212 + return opts ? deleteOrderRequest(opts) : undefined; 213 + }); 191 214 } 192 215 193 216 /** ··· 195 218 * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. 196 219 */ 197 220 public getOrderById<ThrowOnError extends boolean = false>( 198 - options: () => Options<GetOrderByIdData, ThrowOnError>, 221 + options: () => Options<GetOrderByIdData, ThrowOnError> | undefined, 199 222 ) { 200 - return httpResource<GetOrderByIdResponse>(() => 201 - getOrderByIdRequest(options()), 202 - ); 223 + return httpResource<GetOrderByIdResponse>(() => { 224 + const opts = options ? options() : undefined; 225 + return opts ? getOrderByIdRequest(opts) : undefined; 226 + }); 203 227 } 204 228 } 205 229 ··· 212 236 * This can only be done by the logged in user. 213 237 */ 214 238 public createUser<ThrowOnError extends boolean = false>( 215 - options?: () => Options<CreateUserData, ThrowOnError>, 239 + options?: () => Options<CreateUserData, ThrowOnError> | undefined, 216 240 ) { 217 - return httpResource<CreateUserResponse>(() => 218 - createUserRequest(options ? options() : undefined), 219 - ); 241 + return httpResource<CreateUserResponse>(() => { 242 + const opts = options ? options() : undefined; 243 + return opts ? createUserRequest(opts) : undefined; 244 + }); 220 245 } 221 246 222 247 /** ··· 224 249 * Creates list of users with given input array. 225 250 */ 226 251 public createUsersWithListInput<ThrowOnError extends boolean = false>( 227 - options?: () => Options<CreateUsersWithListInputData, ThrowOnError>, 252 + options?: () => 253 + | Options<CreateUsersWithListInputData, ThrowOnError> 254 + | undefined, 228 255 ) { 229 - return httpResource<CreateUsersWithListInputResponse>(() => 230 - createUsersWithListInputRequest(options ? options() : undefined), 231 - ); 256 + return httpResource<CreateUsersWithListInputResponse>(() => { 257 + const opts = options ? options() : undefined; 258 + return opts ? createUsersWithListInputRequest(opts) : undefined; 259 + }); 232 260 } 233 261 234 262 /** ··· 236 264 * Log into the system. 237 265 */ 238 266 public loginUser<ThrowOnError extends boolean = false>( 239 - options?: () => Options<LoginUserData, ThrowOnError>, 267 + options?: () => Options<LoginUserData, ThrowOnError> | undefined, 240 268 ) { 241 - return httpResource<LoginUserResponse>(() => 242 - loginUserRequest(options ? options() : undefined), 243 - ); 269 + return httpResource<LoginUserResponse>(() => { 270 + const opts = options ? options() : undefined; 271 + return opts ? loginUserRequest(opts) : undefined; 272 + }); 244 273 } 245 274 246 275 /** ··· 248 277 * Log user out of the system. 249 278 */ 250 279 public logoutUser<ThrowOnError extends boolean = false>( 251 - options?: () => Options<LogoutUserData, ThrowOnError>, 280 + options?: () => Options<LogoutUserData, ThrowOnError> | undefined, 252 281 ) { 253 - return httpResource<unknown>(() => 254 - logoutUserRequest(options ? options() : undefined), 255 - ); 282 + return httpResource<unknown>(() => { 283 + const opts = options ? options() : undefined; 284 + return opts ? logoutUserRequest(opts) : undefined; 285 + }); 256 286 } 257 287 258 288 /** ··· 260 290 * This can only be done by the logged in user. 261 291 */ 262 292 public deleteUser<ThrowOnError extends boolean = false>( 263 - options: () => Options<DeleteUserData, ThrowOnError>, 293 + options: () => Options<DeleteUserData, ThrowOnError> | undefined, 264 294 ) { 265 - return httpResource<unknown>(() => deleteUserRequest(options())); 295 + return httpResource<unknown>(() => { 296 + const opts = options ? options() : undefined; 297 + return opts ? deleteUserRequest(opts) : undefined; 298 + }); 266 299 } 267 300 268 301 /** ··· 270 303 * Get user detail based on username. 271 304 */ 272 305 public getUserByName<ThrowOnError extends boolean = false>( 273 - options: () => Options<GetUserByNameData, ThrowOnError>, 306 + options: () => Options<GetUserByNameData, ThrowOnError> | undefined, 274 307 ) { 275 - return httpResource<GetUserByNameResponse>(() => 276 - getUserByNameRequest(options()), 277 - ); 308 + return httpResource<GetUserByNameResponse>(() => { 309 + const opts = options ? options() : undefined; 310 + return opts ? getUserByNameRequest(opts) : undefined; 311 + }); 278 312 } 279 313 280 314 /** ··· 282 316 * This can only be done by the logged in user. 283 317 */ 284 318 public updateUser<ThrowOnError extends boolean = false>( 285 - options: () => Options<UpdateUserData, ThrowOnError>, 319 + options: () => Options<UpdateUserData, ThrowOnError> | undefined, 286 320 ) { 287 - return httpResource<unknown>(() => updateUserRequest(options())); 321 + return httpResource<unknown>(() => { 322 + const opts = options ? options() : undefined; 323 + return opts ? updateUserRequest(opts) : undefined; 324 + }); 288 325 } 289 326 }
+62 -25
examples/openapi-ts-angular-common/src/client/client/client.gen.ts
··· 16 16 import { firstValueFrom } from 'rxjs'; 17 17 import { filter } from 'rxjs/operators'; 18 18 19 + import { createSseClient } from '../core/serverSentEvents.gen'; 20 + import type { HttpMethod } from '../core/types.gen'; 19 21 import type { 20 22 Client, 21 23 Config, ··· 60 62 ThrowOnError extends boolean = false, 61 63 TResponseStyle extends ResponseStyle = 'fields', 62 64 >( 63 - options: RequestOptions<TResponseStyle, ThrowOnError>, 65 + options: RequestOptions<unknown, TResponseStyle, ThrowOnError>, 64 66 ) => { 65 67 const opts = { 66 68 ..._config, 67 69 ...options, 68 70 headers: mergeHeaders(_config.headers, options.headers), 69 71 httpClient: options.httpClient ?? _config.httpClient, 70 - method: 'GET', 71 72 serializedBody: options.body as any, 72 73 }; 73 74 ··· 94 95 const url = buildUrl(opts as any); 95 96 96 97 const req = new HttpRequest<unknown>( 97 - opts.method, 98 + opts.method ?? 'GET', 98 99 url, 99 100 opts.serializedBody || null, 100 101 { ··· 103 104 }, 104 105 ); 105 106 106 - return { opts, req }; 107 + return { opts, req, url }; 107 108 }; 108 109 109 - const request: Client['request'] = async (options) => { 110 - const { opts, req: initialReq } = requestOptions(options); 110 + const beforeRequest = async (options: RequestOptions) => { 111 + const { opts, req, url } = requestOptions(options); 111 112 112 113 if (opts.security) { 113 114 await setAuthParams({ ··· 120 121 await opts.requestValidator(opts); 121 122 } 122 123 124 + return { opts, req, url }; 125 + }; 126 + 127 + const request: Client['request'] = async (options) => { 128 + // @ts-expect-error 129 + const { opts, req: initialReq } = await beforeRequest(options); 130 + 123 131 let req = initialReq; 124 132 125 133 for (const fn of interceptors.request._fns) { ··· 128 136 } 129 137 } 130 138 131 - let response; 132 - const result = { 139 + const result: { 140 + request: HttpRequest<unknown>; 141 + response: any; 142 + } = { 133 143 request: req, 134 - response, 144 + response: null, 135 145 }; 136 146 137 147 try { 138 - response = await firstValueFrom( 148 + result.response = (await firstValueFrom( 139 149 opts 140 150 .httpClient!.request(req) 141 151 .pipe(filter((event) => event.type === HttpEventType.Response)), 142 - ); 152 + )) as HttpResponse<unknown>; 143 153 144 154 for (const fn of interceptors.response._fns) { 145 155 if (fn) { 146 - response = await fn(response, req, opts as any); 156 + result.response = await fn(result.response, req, opts as any); 147 157 } 148 158 } 149 159 150 - let bodyResponse: any = response.body; 160 + let bodyResponse = result.response.body; 151 161 152 162 if (opts.responseValidator) { 153 163 await opts.responseValidator(bodyResponse); ··· 162 172 : { data: bodyResponse, ...result }; 163 173 } catch (error) { 164 174 if (error instanceof HttpErrorResponse) { 165 - response = error; 175 + result.response = error; 166 176 } 167 177 168 178 let finalError = error instanceof HttpErrorResponse ? error.error : error; ··· 171 181 if (fn) { 172 182 finalError = (await fn( 173 183 finalError, 174 - response as HttpResponse<unknown>, 184 + result.response as any, 175 185 req, 176 186 opts as any, 177 187 )) as string; ··· 191 201 } 192 202 }; 193 203 204 + const makeMethodFn = 205 + (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 206 + request({ ...options, method }); 207 + 208 + const makeSseFn = 209 + (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 210 + const { opts, url } = await beforeRequest(options); 211 + return createSseClient({ 212 + ...opts, 213 + body: opts.body as BodyInit | null | undefined, 214 + headers: opts.headers as unknown as Record<string, string>, 215 + method, 216 + url, 217 + }); 218 + }; 219 + 194 220 return { 195 221 buildUrl, 196 - connect: (options) => request({ ...options, method: 'CONNECT' }), 197 - delete: (options) => request({ ...options, method: 'DELETE' }), 198 - get: (options) => request({ ...options, method: 'GET' }), 222 + connect: makeMethodFn('CONNECT'), 223 + delete: makeMethodFn('DELETE'), 224 + get: makeMethodFn('GET'), 199 225 getConfig, 200 - head: (options) => request({ ...options, method: 'HEAD' }), 226 + head: makeMethodFn('HEAD'), 201 227 interceptors, 202 - options: (options) => request({ ...options, method: 'OPTIONS' }), 203 - patch: (options) => request({ ...options, method: 'PATCH' }), 204 - post: (options) => request({ ...options, method: 'POST' }), 205 - put: (options) => request({ ...options, method: 'PUT' }), 228 + options: makeMethodFn('OPTIONS'), 229 + patch: makeMethodFn('PATCH'), 230 + post: makeMethodFn('POST'), 231 + put: makeMethodFn('PUT'), 206 232 request, 207 233 requestOptions: (options) => { 208 234 if (options.security) { ··· 218 244 return requestOptions(options).req; 219 245 }, 220 246 setConfig, 221 - trace: (options) => request({ ...options, method: 'TRACE' }), 222 - }; 247 + sse: { 248 + connect: makeSseFn('CONNECT'), 249 + delete: makeSseFn('DELETE'), 250 + get: makeSseFn('GET'), 251 + head: makeSseFn('HEAD'), 252 + options: makeSseFn('OPTIONS'), 253 + patch: makeSseFn('PATCH'), 254 + post: makeSseFn('POST'), 255 + put: makeSseFn('PUT'), 256 + trace: makeSseFn('TRACE'), 257 + }, 258 + trace: makeMethodFn('TRACE'), 259 + } as Client; 223 260 };
+52 -17
examples/openapi-ts-angular-common/src/client/client/types.gen.ts
··· 11 11 12 12 import type { Auth } from '../core/auth.gen'; 13 13 import type { 14 + ServerSentEventsOptions, 15 + ServerSentEventsResult, 16 + } from '../core/serverSentEvents.gen'; 17 + import type { 14 18 Client as CoreClient, 15 19 Config as CoreConfig, 16 20 } from '../core/types.gen'; ··· 63 67 } 64 68 65 69 export interface RequestOptions< 70 + TData = unknown, 66 71 TResponseStyle extends ResponseStyle = 'fields', 67 72 ThrowOnError extends boolean = boolean, 68 73 Url extends string = string, 69 74 > extends Config<{ 70 - responseStyle: TResponseStyle; 71 - throwOnError: ThrowOnError; 72 - }> { 75 + responseStyle: TResponseStyle; 76 + throwOnError: ThrowOnError; 77 + }>, 78 + Pick< 79 + ServerSentEventsOptions<TData>, 80 + | 'onSseError' 81 + | 'onSseEvent' 82 + | 'sseDefaultRetryDelay' 83 + | 'sseMaxRetryAttempts' 84 + | 'sseMaxRetryDelay' 85 + > { 73 86 /** 74 87 * Any body that you want to add to your request. 75 88 * ··· 93 106 TResponseStyle extends ResponseStyle = 'fields', 94 107 ThrowOnError extends boolean = boolean, 95 108 Url extends string = string, 96 - > extends RequestOptions<TResponseStyle, ThrowOnError, Url> { 109 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 97 110 serializedBody?: string; 98 111 } 99 112 ··· 150 163 ThrowOnError extends boolean = false, 151 164 TResponseStyle extends ResponseStyle = 'fields', 152 165 >( 153 - options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 166 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 154 167 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 155 168 169 + type SseFn = < 170 + TData = unknown, 171 + TError = unknown, 172 + ThrowOnError extends boolean = false, 173 + TResponseStyle extends ResponseStyle = 'fields', 174 + >( 175 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 176 + ) => Promise<ServerSentEventsResult<TData, TError>>; 177 + 156 178 type RequestFn = < 157 179 TData = unknown, 158 180 TError = unknown, 159 181 ThrowOnError extends boolean = false, 160 182 TResponseStyle extends ResponseStyle = 'fields', 161 183 >( 162 - options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 163 - Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 184 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 185 + Pick< 186 + Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 187 + 'method' 188 + >, 164 189 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 165 190 166 191 type RequestOptionsFn = < 167 192 ThrowOnError extends boolean = false, 168 193 TResponseStyle extends ResponseStyle = 'fields', 169 194 >( 170 - options: RequestOptions<TResponseStyle, ThrowOnError>, 195 + options: RequestOptions<unknown, TResponseStyle, ThrowOnError>, 171 196 ) => HttpRequest<unknown>; 172 197 173 198 type BuildUrlFn = < ··· 181 206 options: Pick<TData, 'url'> & Options<TData>, 182 207 ) => string; 183 208 184 - export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 209 + export type Client = CoreClient< 210 + RequestFn, 211 + Config, 212 + MethodFn, 213 + BuildUrlFn, 214 + SseFn 215 + > & { 185 216 interceptors: Middleware< 186 217 HttpRequest<unknown>, 187 218 HttpResponse<unknown>, 188 219 unknown, 189 220 ResolvedRequestOptions 190 221 >; 191 - 192 222 requestOptions: RequestOptionsFn; 193 223 }; 194 224 ··· 217 247 export type Options< 218 248 TData extends TDataShape = TDataShape, 219 249 ThrowOnError extends boolean = boolean, 250 + TResponse = unknown, 220 251 TResponseStyle extends ResponseStyle = 'fields', 221 252 > = OmitKeys< 222 - RequestOptions<TResponseStyle, ThrowOnError>, 253 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 223 254 'body' | 'path' | 'query' | 'url' 224 255 > & 225 256 Omit<TData, 'url'>; ··· 231 262 > = TData extends { body?: any } 232 263 ? TData extends { headers?: any } 233 264 ? OmitKeys< 234 - RequestOptions<TResponseStyle, ThrowOnError>, 265 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 235 266 'body' | 'headers' | 'url' 236 267 > & 237 268 TData 238 - : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 269 + : OmitKeys< 270 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 271 + 'body' | 'url' 272 + > & 239 273 TData & 240 - Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 274 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'> 241 275 : TData extends { headers?: any } 242 276 ? OmitKeys< 243 - RequestOptions<TResponseStyle, ThrowOnError>, 277 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 244 278 'headers' | 'url' 245 279 > & 246 280 TData & 247 - Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 248 - : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData; 281 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'> 282 + : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> & 283 + TData;
+264
examples/openapi-ts-angular-common/src/client/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit< 6 + RequestInit, 7 + 'method' 8 + > & 9 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 10 + /** 11 + * Fetch API implementation. You can use this option to provide a custom 12 + * fetch instance. 13 + * 14 + * @default globalThis.fetch 15 + */ 16 + fetch?: typeof fetch; 17 + /** 18 + * Implementing clients can call request interceptors inside this hook. 19 + */ 20 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 21 + /** 22 + * Callback invoked when a network or parsing error occurs during streaming. 23 + * 24 + * This option applies only if the endpoint returns a stream of events. 25 + * 26 + * @param error The error that occurred. 27 + */ 28 + onSseError?: (error: unknown) => void; 29 + /** 30 + * Callback invoked when an event is streamed from the server. 31 + * 32 + * This option applies only if the endpoint returns a stream of events. 33 + * 34 + * @param event Event streamed from the server. 35 + * @returns Nothing (void). 36 + */ 37 + onSseEvent?: (event: StreamEvent<TData>) => void; 38 + serializedBody?: RequestInit['body']; 39 + /** 40 + * Default retry delay in milliseconds. 41 + * 42 + * This option applies only if the endpoint returns a stream of events. 43 + * 44 + * @default 3000 45 + */ 46 + sseDefaultRetryDelay?: number; 47 + /** 48 + * Maximum number of retry attempts before giving up. 49 + */ 50 + sseMaxRetryAttempts?: number; 51 + /** 52 + * Maximum retry delay in milliseconds. 53 + * 54 + * Applies only when exponential backoff is used. 55 + * 56 + * This option applies only if the endpoint returns a stream of events. 57 + * 58 + * @default 30000 59 + */ 60 + sseMaxRetryDelay?: number; 61 + /** 62 + * Optional sleep function for retry backoff. 63 + * 64 + * Defaults to using `setTimeout`. 65 + */ 66 + sseSleepFn?: (ms: number) => Promise<void>; 67 + url: string; 68 + }; 69 + 70 + export interface StreamEvent<TData = unknown> { 71 + data: TData; 72 + event?: string; 73 + id?: string; 74 + retry?: number; 75 + } 76 + 77 + export type ServerSentEventsResult< 78 + TData = unknown, 79 + TReturn = void, 80 + TNext = unknown, 81 + > = { 82 + stream: AsyncGenerator< 83 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 84 + TReturn, 85 + TNext 86 + >; 87 + }; 88 + 89 + export const createSseClient = <TData = unknown>({ 90 + onRequest, 91 + onSseError, 92 + onSseEvent, 93 + responseTransformer, 94 + responseValidator, 95 + sseDefaultRetryDelay, 96 + sseMaxRetryAttempts, 97 + sseMaxRetryDelay, 98 + sseSleepFn, 99 + url, 100 + ...options 101 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 102 + let lastEventId: string | undefined; 103 + 104 + const sleep = 105 + sseSleepFn ?? 106 + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 107 + 108 + const createStream = async function* () { 109 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 110 + let attempt = 0; 111 + const signal = options.signal ?? new AbortController().signal; 112 + 113 + while (true) { 114 + if (signal.aborted) break; 115 + 116 + attempt++; 117 + 118 + const headers = 119 + options.headers instanceof Headers 120 + ? options.headers 121 + : new Headers(options.headers as Record<string, string> | undefined); 122 + 123 + if (lastEventId !== undefined) { 124 + headers.set('Last-Event-ID', lastEventId); 125 + } 126 + 127 + try { 128 + const requestInit: RequestInit = { 129 + redirect: 'follow', 130 + ...options, 131 + body: options.serializedBody, 132 + headers, 133 + signal, 134 + }; 135 + let request = new Request(url, requestInit); 136 + if (onRequest) { 137 + request = await onRequest(url, requestInit); 138 + } 139 + // fetch must be assigned here, otherwise it would throw the error: 140 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 141 + const _fetch = options.fetch ?? globalThis.fetch; 142 + const response = await _fetch(request); 143 + 144 + if (!response.ok) 145 + throw new Error( 146 + `SSE failed: ${response.status} ${response.statusText}`, 147 + ); 148 + 149 + if (!response.body) throw new Error('No body in SSE response'); 150 + 151 + const reader = response.body 152 + .pipeThrough(new TextDecoderStream()) 153 + .getReader(); 154 + 155 + let buffer = ''; 156 + 157 + const abortHandler = () => { 158 + try { 159 + reader.cancel(); 160 + } catch { 161 + // noop 162 + } 163 + }; 164 + 165 + signal.addEventListener('abort', abortHandler); 166 + 167 + try { 168 + while (true) { 169 + const { done, value } = await reader.read(); 170 + if (done) break; 171 + buffer += value; 172 + 173 + const chunks = buffer.split('\n\n'); 174 + buffer = chunks.pop() ?? ''; 175 + 176 + for (const chunk of chunks) { 177 + const lines = chunk.split('\n'); 178 + const dataLines: Array<string> = []; 179 + let eventName: string | undefined; 180 + 181 + for (const line of lines) { 182 + if (line.startsWith('data:')) { 183 + dataLines.push(line.replace(/^data:\s*/, '')); 184 + } else if (line.startsWith('event:')) { 185 + eventName = line.replace(/^event:\s*/, ''); 186 + } else if (line.startsWith('id:')) { 187 + lastEventId = line.replace(/^id:\s*/, ''); 188 + } else if (line.startsWith('retry:')) { 189 + const parsed = Number.parseInt( 190 + line.replace(/^retry:\s*/, ''), 191 + 10, 192 + ); 193 + if (!Number.isNaN(parsed)) { 194 + retryDelay = parsed; 195 + } 196 + } 197 + } 198 + 199 + let data: unknown; 200 + let parsedJson = false; 201 + 202 + if (dataLines.length) { 203 + const rawData = dataLines.join('\n'); 204 + try { 205 + data = JSON.parse(rawData); 206 + parsedJson = true; 207 + } catch { 208 + data = rawData; 209 + } 210 + } 211 + 212 + if (parsedJson) { 213 + if (responseValidator) { 214 + await responseValidator(data); 215 + } 216 + 217 + if (responseTransformer) { 218 + data = await responseTransformer(data); 219 + } 220 + } 221 + 222 + onSseEvent?.({ 223 + data, 224 + event: eventName, 225 + id: lastEventId, 226 + retry: retryDelay, 227 + }); 228 + 229 + if (dataLines.length) { 230 + yield data as any; 231 + } 232 + } 233 + } 234 + } finally { 235 + signal.removeEventListener('abort', abortHandler); 236 + reader.releaseLock(); 237 + } 238 + 239 + break; // exit loop on normal completion 240 + } catch (error) { 241 + // connection failed or aborted; retry after delay 242 + onSseError?.(error); 243 + 244 + if ( 245 + sseMaxRetryAttempts !== undefined && 246 + attempt >= sseMaxRetryAttempts 247 + ) { 248 + break; // stop after firing error 249 + } 250 + 251 + // exponential backoff: double retry each attempt, cap at 30s 252 + const backoff = Math.min( 253 + retryDelay * 2 ** (attempt - 1), 254 + sseMaxRetryDelay ?? 30000, 255 + ); 256 + await sleep(backoff); 257 + } 258 + } 259 + }; 260 + 261 + const stream = createStream(); 262 + 263 + return { stream }; 264 + };
+20 -22
examples/openapi-ts-angular-common/src/client/core/types.gen.ts
··· 7 7 QuerySerializerOptions, 8 8 } from './bodySerializer.gen'; 9 9 10 - export interface Client< 10 + export type HttpMethod = 11 + | 'connect' 12 + | 'delete' 13 + | 'get' 14 + | 'head' 15 + | 'options' 16 + | 'patch' 17 + | 'post' 18 + | 'put' 19 + | 'trace'; 20 + 21 + export type Client< 11 22 RequestFn = never, 12 23 Config = unknown, 13 24 MethodFn = never, 14 25 BuildUrlFn = never, 15 - > { 26 + SseFn = never, 27 + > = { 16 28 /** 17 29 * Returns the final request URL. 18 30 */ 19 31 buildUrl: BuildUrlFn; 20 - connect: MethodFn; 21 - delete: MethodFn; 22 - get: MethodFn; 23 32 getConfig: () => Config; 24 - head: MethodFn; 25 - options: MethodFn; 26 - patch: MethodFn; 27 - post: MethodFn; 28 - put: MethodFn; 29 33 request: RequestFn; 30 34 setConfig: (config: Config) => Config; 31 - trace: MethodFn; 32 - } 35 + } & { 36 + [K in HttpMethod]: MethodFn; 37 + } & ([SseFn] extends [never] 38 + ? { sse?: never } 39 + : { sse: { [K in HttpMethod]: SseFn } }); 33 40 34 41 export interface Config { 35 42 /** ··· 65 72 * 66 73 * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 67 74 */ 68 - method?: 69 - | 'CONNECT' 70 - | 'DELETE' 71 - | 'GET' 72 - | 'HEAD' 73 - | 'OPTIONS' 74 - | 'PATCH' 75 - | 'POST' 76 - | 'PUT' 77 - | 'TRACE'; 75 + method?: Uppercase<HttpMethod>; 78 76 /** 79 77 * A function for serializing request query parameters. By default, arrays 80 78 * will be exploded in form style, objects will be exploded in deepObject
+114
examples/openapi-ts-angular-common/src/client/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace( 48 + match, 49 + serializeArrayParam({ explode, name, style, value }), 50 + ); 51 + continue; 52 + } 53 + 54 + if (typeof value === 'object') { 55 + url = url.replace( 56 + match, 57 + serializeObjectParam({ 58 + explode, 59 + name, 60 + style, 61 + value: value as Record<string, unknown>, 62 + valueOnly: true, 63 + }), 64 + ); 65 + continue; 66 + } 67 + 68 + if (style === 'matrix') { 69 + url = url.replace( 70 + match, 71 + `;${serializePrimitiveParam({ 72 + name, 73 + value: value as string, 74 + })}`, 75 + ); 76 + continue; 77 + } 78 + 79 + const replaceValue = encodeURIComponent( 80 + style === 'label' ? `.${value as string}` : (value as string), 81 + ); 82 + url = url.replace(match, replaceValue); 83 + } 84 + } 85 + return url; 86 + }; 87 + 88 + export const getUrl = ({ 89 + baseUrl, 90 + path, 91 + query, 92 + querySerializer, 93 + url: _url, 94 + }: { 95 + baseUrl?: string; 96 + path?: Record<string, unknown>; 97 + query?: Record<string, unknown>; 98 + querySerializer: QuerySerializer; 99 + url: string; 100 + }) => { 101 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 102 + let url = (baseUrl ?? '') + pathUrl; 103 + if (path) { 104 + url = defaultPathSerializer({ path, url }); 105 + } 106 + let search = query ? querySerializer(query) : ''; 107 + if (search.startsWith('?')) { 108 + search = search.substring(1); 109 + } 110 + if (search) { 111 + url += `?${search}`; 112 + } 113 + return url; 114 + };
+62 -25
examples/openapi-ts-angular/src/client/client/client.gen.ts
··· 16 16 import { firstValueFrom } from 'rxjs'; 17 17 import { filter } from 'rxjs/operators'; 18 18 19 + import { createSseClient } from '../core/serverSentEvents.gen'; 20 + import type { HttpMethod } from '../core/types.gen'; 19 21 import type { 20 22 Client, 21 23 Config, ··· 60 62 ThrowOnError extends boolean = false, 61 63 TResponseStyle extends ResponseStyle = 'fields', 62 64 >( 63 - options: RequestOptions<TResponseStyle, ThrowOnError>, 65 + options: RequestOptions<unknown, TResponseStyle, ThrowOnError>, 64 66 ) => { 65 67 const opts = { 66 68 ..._config, 67 69 ...options, 68 70 headers: mergeHeaders(_config.headers, options.headers), 69 71 httpClient: options.httpClient ?? _config.httpClient, 70 - method: 'GET', 71 72 serializedBody: options.body as any, 72 73 }; 73 74 ··· 94 95 const url = buildUrl(opts as any); 95 96 96 97 const req = new HttpRequest<unknown>( 97 - opts.method, 98 + opts.method ?? 'GET', 98 99 url, 99 100 opts.serializedBody || null, 100 101 { ··· 103 104 }, 104 105 ); 105 106 106 - return { opts, req }; 107 + return { opts, req, url }; 107 108 }; 108 109 109 - const request: Client['request'] = async (options) => { 110 - const { opts, req: initialReq } = requestOptions(options); 110 + const beforeRequest = async (options: RequestOptions) => { 111 + const { opts, req, url } = requestOptions(options); 111 112 112 113 if (opts.security) { 113 114 await setAuthParams({ ··· 120 121 await opts.requestValidator(opts); 121 122 } 122 123 124 + return { opts, req, url }; 125 + }; 126 + 127 + const request: Client['request'] = async (options) => { 128 + // @ts-expect-error 129 + const { opts, req: initialReq } = await beforeRequest(options); 130 + 123 131 let req = initialReq; 124 132 125 133 for (const fn of interceptors.request._fns) { ··· 128 136 } 129 137 } 130 138 131 - let response; 132 - const result = { 139 + const result: { 140 + request: HttpRequest<unknown>; 141 + response: any; 142 + } = { 133 143 request: req, 134 - response, 144 + response: null, 135 145 }; 136 146 137 147 try { 138 - response = await firstValueFrom( 148 + result.response = (await firstValueFrom( 139 149 opts 140 150 .httpClient!.request(req) 141 151 .pipe(filter((event) => event.type === HttpEventType.Response)), 142 - ); 152 + )) as HttpResponse<unknown>; 143 153 144 154 for (const fn of interceptors.response._fns) { 145 155 if (fn) { 146 - response = await fn(response, req, opts as any); 156 + result.response = await fn(result.response, req, opts as any); 147 157 } 148 158 } 149 159 150 - let bodyResponse: any = response.body; 160 + let bodyResponse = result.response.body; 151 161 152 162 if (opts.responseValidator) { 153 163 await opts.responseValidator(bodyResponse); ··· 162 172 : { data: bodyResponse, ...result }; 163 173 } catch (error) { 164 174 if (error instanceof HttpErrorResponse) { 165 - response = error; 175 + result.response = error; 166 176 } 167 177 168 178 let finalError = error instanceof HttpErrorResponse ? error.error : error; ··· 171 181 if (fn) { 172 182 finalError = (await fn( 173 183 finalError, 174 - response as HttpResponse<unknown>, 184 + result.response as any, 175 185 req, 176 186 opts as any, 177 187 )) as string; ··· 191 201 } 192 202 }; 193 203 204 + const makeMethodFn = 205 + (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 206 + request({ ...options, method }); 207 + 208 + const makeSseFn = 209 + (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 210 + const { opts, url } = await beforeRequest(options); 211 + return createSseClient({ 212 + ...opts, 213 + body: opts.body as BodyInit | null | undefined, 214 + headers: opts.headers as unknown as Record<string, string>, 215 + method, 216 + url, 217 + }); 218 + }; 219 + 194 220 return { 195 221 buildUrl, 196 - connect: (options) => request({ ...options, method: 'CONNECT' }), 197 - delete: (options) => request({ ...options, method: 'DELETE' }), 198 - get: (options) => request({ ...options, method: 'GET' }), 222 + connect: makeMethodFn('CONNECT'), 223 + delete: makeMethodFn('DELETE'), 224 + get: makeMethodFn('GET'), 199 225 getConfig, 200 - head: (options) => request({ ...options, method: 'HEAD' }), 226 + head: makeMethodFn('HEAD'), 201 227 interceptors, 202 - options: (options) => request({ ...options, method: 'OPTIONS' }), 203 - patch: (options) => request({ ...options, method: 'PATCH' }), 204 - post: (options) => request({ ...options, method: 'POST' }), 205 - put: (options) => request({ ...options, method: 'PUT' }), 228 + options: makeMethodFn('OPTIONS'), 229 + patch: makeMethodFn('PATCH'), 230 + post: makeMethodFn('POST'), 231 + put: makeMethodFn('PUT'), 206 232 request, 207 233 requestOptions: (options) => { 208 234 if (options.security) { ··· 218 244 return requestOptions(options).req; 219 245 }, 220 246 setConfig, 221 - trace: (options) => request({ ...options, method: 'TRACE' }), 222 - }; 247 + sse: { 248 + connect: makeSseFn('CONNECT'), 249 + delete: makeSseFn('DELETE'), 250 + get: makeSseFn('GET'), 251 + head: makeSseFn('HEAD'), 252 + options: makeSseFn('OPTIONS'), 253 + patch: makeSseFn('PATCH'), 254 + post: makeSseFn('POST'), 255 + put: makeSseFn('PUT'), 256 + trace: makeSseFn('TRACE'), 257 + }, 258 + trace: makeMethodFn('TRACE'), 259 + } as Client; 223 260 };
+52 -17
examples/openapi-ts-angular/src/client/client/types.gen.ts
··· 11 11 12 12 import type { Auth } from '../core/auth.gen'; 13 13 import type { 14 + ServerSentEventsOptions, 15 + ServerSentEventsResult, 16 + } from '../core/serverSentEvents.gen'; 17 + import type { 14 18 Client as CoreClient, 15 19 Config as CoreConfig, 16 20 } from '../core/types.gen'; ··· 63 67 } 64 68 65 69 export interface RequestOptions< 70 + TData = unknown, 66 71 TResponseStyle extends ResponseStyle = 'fields', 67 72 ThrowOnError extends boolean = boolean, 68 73 Url extends string = string, 69 74 > extends Config<{ 70 - responseStyle: TResponseStyle; 71 - throwOnError: ThrowOnError; 72 - }> { 75 + responseStyle: TResponseStyle; 76 + throwOnError: ThrowOnError; 77 + }>, 78 + Pick< 79 + ServerSentEventsOptions<TData>, 80 + | 'onSseError' 81 + | 'onSseEvent' 82 + | 'sseDefaultRetryDelay' 83 + | 'sseMaxRetryAttempts' 84 + | 'sseMaxRetryDelay' 85 + > { 73 86 /** 74 87 * Any body that you want to add to your request. 75 88 * ··· 93 106 TResponseStyle extends ResponseStyle = 'fields', 94 107 ThrowOnError extends boolean = boolean, 95 108 Url extends string = string, 96 - > extends RequestOptions<TResponseStyle, ThrowOnError, Url> { 109 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 97 110 serializedBody?: string; 98 111 } 99 112 ··· 150 163 ThrowOnError extends boolean = false, 151 164 TResponseStyle extends ResponseStyle = 'fields', 152 165 >( 153 - options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 166 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 154 167 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 155 168 169 + type SseFn = < 170 + TData = unknown, 171 + TError = unknown, 172 + ThrowOnError extends boolean = false, 173 + TResponseStyle extends ResponseStyle = 'fields', 174 + >( 175 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 176 + ) => Promise<ServerSentEventsResult<TData, TError>>; 177 + 156 178 type RequestFn = < 157 179 TData = unknown, 158 180 TError = unknown, 159 181 ThrowOnError extends boolean = false, 160 182 TResponseStyle extends ResponseStyle = 'fields', 161 183 >( 162 - options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 163 - Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 184 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 185 + Pick< 186 + Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 187 + 'method' 188 + >, 164 189 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 165 190 166 191 type RequestOptionsFn = < 167 192 ThrowOnError extends boolean = false, 168 193 TResponseStyle extends ResponseStyle = 'fields', 169 194 >( 170 - options: RequestOptions<TResponseStyle, ThrowOnError>, 195 + options: RequestOptions<unknown, TResponseStyle, ThrowOnError>, 171 196 ) => HttpRequest<unknown>; 172 197 173 198 type BuildUrlFn = < ··· 181 206 options: Pick<TData, 'url'> & Options<TData>, 182 207 ) => string; 183 208 184 - export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 209 + export type Client = CoreClient< 210 + RequestFn, 211 + Config, 212 + MethodFn, 213 + BuildUrlFn, 214 + SseFn 215 + > & { 185 216 interceptors: Middleware< 186 217 HttpRequest<unknown>, 187 218 HttpResponse<unknown>, 188 219 unknown, 189 220 ResolvedRequestOptions 190 221 >; 191 - 192 222 requestOptions: RequestOptionsFn; 193 223 }; 194 224 ··· 217 247 export type Options< 218 248 TData extends TDataShape = TDataShape, 219 249 ThrowOnError extends boolean = boolean, 250 + TResponse = unknown, 220 251 TResponseStyle extends ResponseStyle = 'fields', 221 252 > = OmitKeys< 222 - RequestOptions<TResponseStyle, ThrowOnError>, 253 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 223 254 'body' | 'path' | 'query' | 'url' 224 255 > & 225 256 Omit<TData, 'url'>; ··· 231 262 > = TData extends { body?: any } 232 263 ? TData extends { headers?: any } 233 264 ? OmitKeys< 234 - RequestOptions<TResponseStyle, ThrowOnError>, 265 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 235 266 'body' | 'headers' | 'url' 236 267 > & 237 268 TData 238 - : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 269 + : OmitKeys< 270 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 271 + 'body' | 'url' 272 + > & 239 273 TData & 240 - Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 274 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'> 241 275 : TData extends { headers?: any } 242 276 ? OmitKeys< 243 - RequestOptions<TResponseStyle, ThrowOnError>, 277 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 244 278 'headers' | 'url' 245 279 > & 246 280 TData & 247 - Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 248 - : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData; 281 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'> 282 + : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> & 283 + TData;
+264
examples/openapi-ts-angular/src/client/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit< 6 + RequestInit, 7 + 'method' 8 + > & 9 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 10 + /** 11 + * Fetch API implementation. You can use this option to provide a custom 12 + * fetch instance. 13 + * 14 + * @default globalThis.fetch 15 + */ 16 + fetch?: typeof fetch; 17 + /** 18 + * Implementing clients can call request interceptors inside this hook. 19 + */ 20 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 21 + /** 22 + * Callback invoked when a network or parsing error occurs during streaming. 23 + * 24 + * This option applies only if the endpoint returns a stream of events. 25 + * 26 + * @param error The error that occurred. 27 + */ 28 + onSseError?: (error: unknown) => void; 29 + /** 30 + * Callback invoked when an event is streamed from the server. 31 + * 32 + * This option applies only if the endpoint returns a stream of events. 33 + * 34 + * @param event Event streamed from the server. 35 + * @returns Nothing (void). 36 + */ 37 + onSseEvent?: (event: StreamEvent<TData>) => void; 38 + serializedBody?: RequestInit['body']; 39 + /** 40 + * Default retry delay in milliseconds. 41 + * 42 + * This option applies only if the endpoint returns a stream of events. 43 + * 44 + * @default 3000 45 + */ 46 + sseDefaultRetryDelay?: number; 47 + /** 48 + * Maximum number of retry attempts before giving up. 49 + */ 50 + sseMaxRetryAttempts?: number; 51 + /** 52 + * Maximum retry delay in milliseconds. 53 + * 54 + * Applies only when exponential backoff is used. 55 + * 56 + * This option applies only if the endpoint returns a stream of events. 57 + * 58 + * @default 30000 59 + */ 60 + sseMaxRetryDelay?: number; 61 + /** 62 + * Optional sleep function for retry backoff. 63 + * 64 + * Defaults to using `setTimeout`. 65 + */ 66 + sseSleepFn?: (ms: number) => Promise<void>; 67 + url: string; 68 + }; 69 + 70 + export interface StreamEvent<TData = unknown> { 71 + data: TData; 72 + event?: string; 73 + id?: string; 74 + retry?: number; 75 + } 76 + 77 + export type ServerSentEventsResult< 78 + TData = unknown, 79 + TReturn = void, 80 + TNext = unknown, 81 + > = { 82 + stream: AsyncGenerator< 83 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 84 + TReturn, 85 + TNext 86 + >; 87 + }; 88 + 89 + export const createSseClient = <TData = unknown>({ 90 + onRequest, 91 + onSseError, 92 + onSseEvent, 93 + responseTransformer, 94 + responseValidator, 95 + sseDefaultRetryDelay, 96 + sseMaxRetryAttempts, 97 + sseMaxRetryDelay, 98 + sseSleepFn, 99 + url, 100 + ...options 101 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 102 + let lastEventId: string | undefined; 103 + 104 + const sleep = 105 + sseSleepFn ?? 106 + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 107 + 108 + const createStream = async function* () { 109 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 110 + let attempt = 0; 111 + const signal = options.signal ?? new AbortController().signal; 112 + 113 + while (true) { 114 + if (signal.aborted) break; 115 + 116 + attempt++; 117 + 118 + const headers = 119 + options.headers instanceof Headers 120 + ? options.headers 121 + : new Headers(options.headers as Record<string, string> | undefined); 122 + 123 + if (lastEventId !== undefined) { 124 + headers.set('Last-Event-ID', lastEventId); 125 + } 126 + 127 + try { 128 + const requestInit: RequestInit = { 129 + redirect: 'follow', 130 + ...options, 131 + body: options.serializedBody, 132 + headers, 133 + signal, 134 + }; 135 + let request = new Request(url, requestInit); 136 + if (onRequest) { 137 + request = await onRequest(url, requestInit); 138 + } 139 + // fetch must be assigned here, otherwise it would throw the error: 140 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 141 + const _fetch = options.fetch ?? globalThis.fetch; 142 + const response = await _fetch(request); 143 + 144 + if (!response.ok) 145 + throw new Error( 146 + `SSE failed: ${response.status} ${response.statusText}`, 147 + ); 148 + 149 + if (!response.body) throw new Error('No body in SSE response'); 150 + 151 + const reader = response.body 152 + .pipeThrough(new TextDecoderStream()) 153 + .getReader(); 154 + 155 + let buffer = ''; 156 + 157 + const abortHandler = () => { 158 + try { 159 + reader.cancel(); 160 + } catch { 161 + // noop 162 + } 163 + }; 164 + 165 + signal.addEventListener('abort', abortHandler); 166 + 167 + try { 168 + while (true) { 169 + const { done, value } = await reader.read(); 170 + if (done) break; 171 + buffer += value; 172 + 173 + const chunks = buffer.split('\n\n'); 174 + buffer = chunks.pop() ?? ''; 175 + 176 + for (const chunk of chunks) { 177 + const lines = chunk.split('\n'); 178 + const dataLines: Array<string> = []; 179 + let eventName: string | undefined; 180 + 181 + for (const line of lines) { 182 + if (line.startsWith('data:')) { 183 + dataLines.push(line.replace(/^data:\s*/, '')); 184 + } else if (line.startsWith('event:')) { 185 + eventName = line.replace(/^event:\s*/, ''); 186 + } else if (line.startsWith('id:')) { 187 + lastEventId = line.replace(/^id:\s*/, ''); 188 + } else if (line.startsWith('retry:')) { 189 + const parsed = Number.parseInt( 190 + line.replace(/^retry:\s*/, ''), 191 + 10, 192 + ); 193 + if (!Number.isNaN(parsed)) { 194 + retryDelay = parsed; 195 + } 196 + } 197 + } 198 + 199 + let data: unknown; 200 + let parsedJson = false; 201 + 202 + if (dataLines.length) { 203 + const rawData = dataLines.join('\n'); 204 + try { 205 + data = JSON.parse(rawData); 206 + parsedJson = true; 207 + } catch { 208 + data = rawData; 209 + } 210 + } 211 + 212 + if (parsedJson) { 213 + if (responseValidator) { 214 + await responseValidator(data); 215 + } 216 + 217 + if (responseTransformer) { 218 + data = await responseTransformer(data); 219 + } 220 + } 221 + 222 + onSseEvent?.({ 223 + data, 224 + event: eventName, 225 + id: lastEventId, 226 + retry: retryDelay, 227 + }); 228 + 229 + if (dataLines.length) { 230 + yield data as any; 231 + } 232 + } 233 + } 234 + } finally { 235 + signal.removeEventListener('abort', abortHandler); 236 + reader.releaseLock(); 237 + } 238 + 239 + break; // exit loop on normal completion 240 + } catch (error) { 241 + // connection failed or aborted; retry after delay 242 + onSseError?.(error); 243 + 244 + if ( 245 + sseMaxRetryAttempts !== undefined && 246 + attempt >= sseMaxRetryAttempts 247 + ) { 248 + break; // stop after firing error 249 + } 250 + 251 + // exponential backoff: double retry each attempt, cap at 30s 252 + const backoff = Math.min( 253 + retryDelay * 2 ** (attempt - 1), 254 + sseMaxRetryDelay ?? 30000, 255 + ); 256 + await sleep(backoff); 257 + } 258 + } 259 + }; 260 + 261 + const stream = createStream(); 262 + 263 + return { stream }; 264 + };
+20 -22
examples/openapi-ts-angular/src/client/core/types.gen.ts
··· 7 7 QuerySerializerOptions, 8 8 } from './bodySerializer.gen'; 9 9 10 - export interface Client< 10 + export type HttpMethod = 11 + | 'connect' 12 + | 'delete' 13 + | 'get' 14 + | 'head' 15 + | 'options' 16 + | 'patch' 17 + | 'post' 18 + | 'put' 19 + | 'trace'; 20 + 21 + export type Client< 11 22 RequestFn = never, 12 23 Config = unknown, 13 24 MethodFn = never, 14 25 BuildUrlFn = never, 15 - > { 26 + SseFn = never, 27 + > = { 16 28 /** 17 29 * Returns the final request URL. 18 30 */ 19 31 buildUrl: BuildUrlFn; 20 - connect: MethodFn; 21 - delete: MethodFn; 22 - get: MethodFn; 23 32 getConfig: () => Config; 24 - head: MethodFn; 25 - options: MethodFn; 26 - patch: MethodFn; 27 - post: MethodFn; 28 - put: MethodFn; 29 33 request: RequestFn; 30 34 setConfig: (config: Config) => Config; 31 - trace: MethodFn; 32 - } 35 + } & { 36 + [K in HttpMethod]: MethodFn; 37 + } & ([SseFn] extends [never] 38 + ? { sse?: never } 39 + : { sse: { [K in HttpMethod]: SseFn } }); 33 40 34 41 export interface Config { 35 42 /** ··· 65 72 * 66 73 * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 67 74 */ 68 - method?: 69 - | 'CONNECT' 70 - | 'DELETE' 71 - | 'GET' 72 - | 'HEAD' 73 - | 'OPTIONS' 74 - | 'PATCH' 75 - | 'POST' 76 - | 'PUT' 77 - | 'TRACE'; 75 + method?: Uppercase<HttpMethod>; 78 76 /** 79 77 * A function for serializing request query parameters. By default, arrays 80 78 * will be exploded in form style, objects will be exploded in deepObject
+114
examples/openapi-ts-angular/src/client/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace( 48 + match, 49 + serializeArrayParam({ explode, name, style, value }), 50 + ); 51 + continue; 52 + } 53 + 54 + if (typeof value === 'object') { 55 + url = url.replace( 56 + match, 57 + serializeObjectParam({ 58 + explode, 59 + name, 60 + style, 61 + value: value as Record<string, unknown>, 62 + valueOnly: true, 63 + }), 64 + ); 65 + continue; 66 + } 67 + 68 + if (style === 'matrix') { 69 + url = url.replace( 70 + match, 71 + `;${serializePrimitiveParam({ 72 + name, 73 + value: value as string, 74 + })}`, 75 + ); 76 + continue; 77 + } 78 + 79 + const replaceValue = encodeURIComponent( 80 + style === 'label' ? `.${value as string}` : (value as string), 81 + ); 82 + url = url.replace(match, replaceValue); 83 + } 84 + } 85 + return url; 86 + }; 87 + 88 + export const getUrl = ({ 89 + baseUrl, 90 + path, 91 + query, 92 + querySerializer, 93 + url: _url, 94 + }: { 95 + baseUrl?: string; 96 + path?: Record<string, unknown>; 97 + query?: Record<string, unknown>; 98 + querySerializer: QuerySerializer; 99 + url: string; 100 + }) => { 101 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 102 + let url = (baseUrl ?? '') + pathUrl; 103 + if (path) { 104 + url = defaultPathSerializer({ path, url }); 105 + } 106 + let search = query ? querySerializer(query) : ''; 107 + if (search.startsWith('?')) { 108 + search = search.substring(1); 109 + } 110 + if (search) { 111 + url += `?${search}`; 112 + } 113 + return url; 114 + };
+87 -30
packages/openapi-ts/src/plugins/@angular/common/httpResources.ts
··· 1 - import type ts from 'typescript'; 1 + import ts from 'typescript'; 2 2 3 3 import type { GeneratedFile } from '../../../generate/file'; 4 4 import type { IR } from '../../../ir/types'; ··· 14 14 } from '../../shared/utils/operation'; 15 15 import { REQUEST_APIS_SUFFIX, RESOURCE_APIS_SUFFIX } from './constants'; 16 16 import type { AngularCommonPlugin } from './types'; 17 + 18 + // Helper function to create a variable statement 19 + const createVariableStatement = ( 20 + name: string, 21 + initializer: ts.Expression, 22 + ): ts.VariableStatement => 23 + ts.factory.createVariableStatement( 24 + undefined, 25 + ts.factory.createVariableDeclarationList( 26 + [ 27 + ts.factory.createVariableDeclaration( 28 + name, 29 + undefined, 30 + undefined, 31 + initializer, 32 + ), 33 + ], 34 + ts.NodeFlags.Const, 35 + ), 36 + ); 17 37 18 38 interface AngularServiceClassEntry { 19 39 className: string; ··· 198 218 199 219 const generateResourceCallExpression = ({ 200 220 file, 201 - isRequiredOptions, 202 221 operation, 203 222 plugin, 204 223 responseTypeName, 205 224 }: { 206 225 file: GeneratedFile; 207 - isRequiredOptions: boolean; 208 226 operation: IR.OperationObject; 209 227 plugin: AngularCommonPlugin['Instance']; 210 228 responseTypeName: string; ··· 213 231 214 232 // Check if httpRequest is configured to use classes 215 233 const useRequestClasses = plugin.config.httpRequests.asClass; 216 - let requestFunctionCall; 217 - 218 - // Create the options call expression based on whether options are required 219 - const optionsCallExpression = isRequiredOptions 220 - ? tsc.callExpression({ 221 - functionName: 'options', 222 - parameters: [], 223 - }) 224 - : tsc.conditionalExpression({ 225 - condition: tsc.identifier({ text: 'options' }), 226 - whenFalse: tsc.identifier({ text: 'undefined' }), 227 - whenTrue: tsc.callExpression({ 228 - functionName: 'options', 229 - parameters: [], 230 - }), 231 - }); 232 234 233 235 if (useRequestClasses) { 234 236 // For class-based request methods, use inject and class hierarchy ··· 278 280 name: requestMethodName, 279 281 }); 280 282 281 - requestFunctionCall = tsc.callExpression({ 282 - functionName: methodAccess, 283 - parameters: [optionsCallExpression], 283 + return tsc.callExpression({ 284 + functionName: 'httpResource', 285 + parameters: [ 286 + tsc.arrowFunction({ 287 + parameters: [], 288 + statements: [ 289 + createVariableStatement( 290 + 'opts', 291 + tsc.conditionalExpression({ 292 + condition: tsc.identifier({ text: 'options' }), 293 + whenFalse: tsc.identifier({ text: 'undefined' }), 294 + whenTrue: tsc.callExpression({ 295 + functionName: 'options', 296 + parameters: [], 297 + }), 298 + }), 299 + ), 300 + tsc.returnStatement({ 301 + expression: tsc.conditionalExpression({ 302 + condition: tsc.identifier({ text: 'opts' }), 303 + whenFalse: tsc.identifier({ text: 'undefined' }), 304 + whenTrue: tsc.callExpression({ 305 + functionName: methodAccess, 306 + parameters: [tsc.identifier({ text: 'opts' })], 307 + }), 308 + }), 309 + }), 310 + ], 311 + }), 312 + ], 313 + types: [tsc.typeNode(responseTypeName)], 284 314 }); 285 315 } 286 316 } else { ··· 296 326 name: requestFunctionName, 297 327 }); 298 328 299 - requestFunctionCall = tsc.callExpression({ 300 - functionName: requestImport.name, 301 - parameters: [optionsCallExpression], 329 + return tsc.callExpression({ 330 + functionName: 'httpResource', 331 + parameters: [ 332 + tsc.arrowFunction({ 333 + parameters: [], 334 + statements: [ 335 + createVariableStatement( 336 + 'opts', 337 + tsc.conditionalExpression({ 338 + condition: tsc.identifier({ text: 'options' }), 339 + whenFalse: tsc.identifier({ text: 'undefined' }), 340 + whenTrue: tsc.callExpression({ 341 + functionName: 'options', 342 + parameters: [], 343 + }), 344 + }), 345 + ), 346 + tsc.returnStatement({ 347 + expression: tsc.conditionalExpression({ 348 + condition: tsc.identifier({ text: 'opts' }), 349 + whenFalse: tsc.identifier({ text: 'undefined' }), 350 + whenTrue: tsc.callExpression({ 351 + functionName: requestImport.name, 352 + parameters: [tsc.identifier({ text: 'opts' })], 353 + }), 354 + }), 355 + }), 356 + ], 357 + }), 358 + ], 359 + types: [tsc.typeNode(responseTypeName)], 302 360 }); 303 361 } 304 362 363 + // Fallback return (should not reach here) 305 364 return tsc.callExpression({ 306 365 functionName: 'httpResource', 307 366 parameters: [ ··· 309 368 parameters: [], 310 369 statements: [ 311 370 tsc.returnStatement({ 312 - expression: requestFunctionCall, 371 + expression: tsc.identifier({ text: 'undefined' }), 313 372 }), 314 373 ], 315 374 }), ··· 360 419 { 361 420 isRequired: isRequiredOptions, 362 421 name: 'options', 363 - type: `() => Options<${dataType.name || 'unknown'}, ThrowOnError>`, 422 + type: `() => Options<${dataType.name || 'unknown'}, ThrowOnError> | undefined`, 364 423 }, 365 424 ], 366 425 returnType: undefined, ··· 368 427 tsc.returnStatement({ 369 428 expression: generateResourceCallExpression({ 370 429 file, 371 - isRequiredOptions, 372 430 operation, 373 431 plugin, 374 432 responseTypeName: responseType.name || 'unknown', ··· 425 483 { 426 484 isRequired: isRequiredOptions, 427 485 name: 'options', 428 - type: `() => Options<${dataType.name || 'unknown'}, ThrowOnError>`, 486 + type: `() => Options<${dataType.name || 'unknown'}, ThrowOnError> | undefined`, 429 487 }, 430 488 ], 431 489 statements: [ 432 490 tsc.returnStatement({ 433 491 expression: generateResourceCallExpression({ 434 492 file, 435 - isRequiredOptions, 436 493 operation, 437 494 plugin, 438 495 responseTypeName: responseType.name || 'unknown',