diff --git a/examples/openapi-ts-angular/openapi-ts.config.ts b/examples/openapi-ts-angular/openapi-ts.config.ts index e5bc5731b..63107e271 100644 --- a/examples/openapi-ts-angular/openapi-ts.config.ts +++ b/examples/openapi-ts-angular/openapi-ts.config.ts @@ -9,26 +9,15 @@ export default defineConfig({ path: './src/client', }, plugins: [ - '@hey-api/client-angular', - '@hey-api/schemas', { - asClass: false, - classNameBuilder(name) { - return `${name}Service`; - }, - methodNameBuilder(operation) { - return String(operation.id); - }, - name: '@hey-api/sdk', + name: '@hey-api/client-angular', + // throwOnError: true, }, + '@hey-api/schemas', + '@hey-api/sdk', { enums: 'javascript', name: '@hey-api/typescript', }, - '@tanstack/angular-query-experimental', - { - asClass: true, - name: '@hey-api/angular-resource', - }, ], }); diff --git a/examples/openapi-ts-angular/src/app/app.component.html b/examples/openapi-ts-angular/src/app/app.component.html index c47306d50..82637a546 100644 --- a/examples/openapi-ts-angular/src/app/app.component.html +++ b/examples/openapi-ts-angular/src/app/app.component.html @@ -243,8 +243,9 @@ -

@hey-api/openapi-ts 🤝 Angular

- +

{{ title }}

+ +
diff --git a/examples/openapi-ts-angular/src/app/app.component.ts b/examples/openapi-ts-angular/src/app/app.component.ts index 69213d755..e5dfbe536 100644 --- a/examples/openapi-ts-angular/src/app/app.component.ts +++ b/examples/openapi-ts-angular/src/app/app.component.ts @@ -1,48 +1,15 @@ -import { HttpClient } from '@angular/common/http'; -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { getPetById } from '../client'; -import { createClient } from '../client/client'; - -const localClient = createClient({ - // set default base url for requests made by this client - baseUrl: 'https://petstore3.swagger.io/api/v3', - /** - * Set default headers only for requests made by this client. This is to - * demonstrate local clients and their configuration taking precedence over - * internal service client. - */ - headers: { - Authorization: 'Bearer ', - }, -}); +import { Demo } from './demo/demo'; @Component({ - imports: [RouterOutlet], + host: { ngSkipHydration: 'true' }, + imports: [RouterOutlet, Demo], selector: 'app-root', styleUrl: './app.component.css', templateUrl: './app.component.html', }) export class AppComponent { - title = 'angular'; - - private http = inject(HttpClient); - - async onGetPetById() { - this.http.get('', {}); - const { data, error } = await getPetById({ - client: localClient, - path: { - // random id 1-10 - petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), - }, - }); - if (error) { - console.log(error); - return; - } - console.log(data); - // setPet(data!); - } + title = '@hey-api/openapi-ts 🤝 Angular'; } diff --git a/examples/openapi-ts-angular/src/app/app.config.ts b/examples/openapi-ts-angular/src/app/app.config.ts index 117bab5e1..47397ce0d 100644 --- a/examples/openapi-ts-angular/src/app/app.config.ts +++ b/examples/openapi-ts-angular/src/app/app.config.ts @@ -7,6 +7,8 @@ import { } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; +import { client } from '../client/client.gen'; +import { provideHeyApiClient } from '../client/client/client.gen'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { @@ -15,5 +17,6 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideClientHydration(withEventReplay()), provideHttpClient(withFetch()), + provideHeyApiClient(client), ], }; diff --git a/examples/openapi-ts-angular/src/app/demo/demo.css b/examples/openapi-ts-angular/src/app/demo/demo.css new file mode 100644 index 000000000..dd26f3c80 --- /dev/null +++ b/examples/openapi-ts-angular/src/app/demo/demo.css @@ -0,0 +1,156 @@ +/* Pet Card Styles */ +.pet-card { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border-radius: 16px; + padding: 1.5rem; + margin-top: 1.5rem; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.05); + transition: + transform 120ms ease, + box-shadow 120ms ease; +} + +.pet-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.pet-avatar { + width: 64px; + height: 64px; + border-radius: 12px; + overflow: hidden; + flex-shrink: 0; + background: var(--red-to-pink-to-purple-horizontal-gradient); + display: flex; + align-items: center; + justify-content: center; +} + +.pet-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.pet-placeholder { + color: white; + font-size: 1.5rem; + font-weight: 600; + text-transform: uppercase; +} + +.pet-details { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.pet-name { + font-size: 1.125rem; + font-weight: 600; + color: var(--gray-900); + margin: 0; +} + +.pet-category { + font-size: 0.875rem; + color: var(--gray-700); + background: rgba(255, 65, 248, 0.1); + padding: 0.25rem 0.75rem; + border-radius: 20px; + display: inline-block; + width: fit-content; +} + +/* Error Message Styles */ +.error-message { + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); + border: 1px solid #fecaca; + border-radius: 12px; + padding: 1rem; + margin-top: 1.5rem; + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1); +} + +.error-title { + font-size: 1rem; + font-weight: 600; + color: #dc2626; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.error-title::before { + content: '⚠️'; + font-size: 1.125rem; +} + +.error-details { + font-size: 0.875rem; + color: #991b1b; + background: rgba(239, 68, 68, 0.05); + padding: 0.75rem; + border-radius: 8px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + white-space: pre-wrap; + word-break: break-word; + border-left: 3px solid #ef4444; +} + +/* Button Styles Enhancement */ +button { + background: var(--red-to-pink-to-purple-horizontal-gradient); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 120ms ease; + margin-top: 1rem; + margin-right: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +button:active { + transform: translateY(0); +} + +/* Responsive adjustments */ +@media screen and (max-width: 650px) { + .pet-card { + padding: 1rem; + margin-top: 1rem; + } + + .pet-info { + flex-direction: column; + text-align: center; + gap: 1rem; + } + + .pet-avatar { + width: 80px; + height: 80px; + align-self: center; + } + + .error-message { + padding: 0.75rem; + margin-top: 1rem; + } +} diff --git a/examples/openapi-ts-angular/src/app/demo/demo.html b/examples/openapi-ts-angular/src/app/demo/demo.html new file mode 100644 index 000000000..dadb12d97 --- /dev/null +++ b/examples/openapi-ts-angular/src/app/demo/demo.html @@ -0,0 +1,37 @@ + + + + +@if (error()) { +
+
Error occurred:
+
{{ error()?.error }}
+
+} + + +@if (pet()) { +
+
+
+ @if (pet()?.photoUrls?.[0]) { + + } @else { +
{{ pet()?.name?.slice(0, 1) || 'N' }}
+ } +
+
+
Name: {{ pet()?.name || 'N/A' }}
+
+ Category: {{ pet()?.category?.name || 'N/A' }} +
+
+
+
+} diff --git a/examples/openapi-ts-angular/src/app/demo/demo.ts b/examples/openapi-ts-angular/src/app/demo/demo.ts new file mode 100644 index 000000000..7497f82e7 --- /dev/null +++ b/examples/openapi-ts-angular/src/app/demo/demo.ts @@ -0,0 +1,95 @@ +import { JsonPipe } from '@angular/common'; +import type { HttpErrorResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; +import { Component, inject, signal } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +import type { AddPetErrors, Pet } from '../../client'; +import { getPetById } from '../../client'; +import { createClient } from '../../client/client'; + +const localClient = createClient({ + // set default base url for requests made by this client + baseUrl: 'https://petstore3.swagger.io/api/v3', + /** + * Set default headers only for requests made by this client. This is to + * demonstrate local clients and their configuration taking precedence over + * internal service client. + */ + headers: { + Authorization: 'Bearer ', + }, +}); + +@Component({ + host: { ngSkipHydration: 'true' }, + imports: [RouterOutlet, JsonPipe], + selector: 'app-demo', + styleUrl: './demo.css', + templateUrl: './demo.html', +}) +export class Demo { + pet = signal(undefined); + error = signal< + | undefined + | { + error: AddPetErrors[keyof AddPetErrors] | Error; + response: HttpErrorResponse; + } + >(undefined); + + #http = inject(HttpClient); + + // // you can set a global httpClient for this client like so, in your app.config, or on each request (like below) + // ngOnInit(): void { + // localClient.setConfig({ + // httpClient: this.#http, + // }); + // } + + onGetPetByIdLocalClient = async () => { + const { data, error, response } = await getPetById({ + client: localClient, + httpClient: this.#http, + path: { + // random id 1-10 + petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), + }, + }); + + if (error) { + console.log(error); + this.error.set({ + error, + response: response as HttpErrorResponse, + }); + return; + } + + this.pet.set(data); + this.error.set(undefined); + }; + + onGetPetById = async () => { + const { data, error, response } = await getPetById({ + path: { + // random id 1-10 + petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), + }, + }); + + if (error) { + console.log(error); + this.error.set({ + error, + response: response as HttpErrorResponse, + }); + return; + } else { + console.log(error); + console.log(response); + this.pet.set(data); + this.error.set(undefined); + } + }; +} diff --git a/examples/openapi-ts-angular/src/client/@hey-api/angular-resource.gen.ts b/examples/openapi-ts-angular/src/client/@hey-api/angular-resource.gen.ts deleted file mode 100644 index 5a96a8d9d..000000000 --- a/examples/openapi-ts-angular/src/client/@hey-api/angular-resource.gen.ts +++ /dev/null @@ -1,309 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { Injectable, resource } from '@angular/core'; - -import type { Options } from '../sdk.gen'; -import { - addPet, - createUser, - createUsersWithListInput, - deleteOrder, - deletePet, - deleteUser, - findPetsByStatus, - findPetsByTags, - getInventory, - getOrderById, - getPetById, - getUserByName, - loginUser, - logoutUser, - placeOrder, - updatePet, - updatePetWithForm, - updateUser, - uploadFile, -} from '../sdk.gen'; -import type { - AddPetData, - CreateUserData, - CreateUsersWithListInputData, - DeleteOrderData, - DeletePetData, - DeleteUserData, - FindPetsByStatusData, - FindPetsByTagsData, - GetInventoryData, - GetOrderByIdData, - GetPetByIdData, - GetUserByNameData, - LoginUserData, - LogoutUserData, - PlaceOrderData, - UpdatePetData, - UpdatePetWithFormData, - UpdateUserData, - UploadFileData, -} from '../types.gen'; - -@Injectable({ - providedIn: 'root', -}) -export class PetServiceResources { - /** - * Add a new pet to the store. - * Add a new pet to the store. - */ - public addPetResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => addPet(params), - params: () => options, - }); - } - - /** - * Update an existing pet. - * Update an existing pet by Id. - */ - public updatePetResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => updatePet(params), - params: () => options, - }); - } - - /** - * Finds Pets by status. - * Multiple status values can be provided with comma separated strings. - */ - public findPetsByStatusResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => findPetsByStatus(params), - params: () => options, - }); - } - - /** - * Finds Pets by tags. - * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - */ - public findPetsByTagsResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => findPetsByTags(params), - params: () => options, - }); - } - - /** - * Deletes a pet. - * Delete a pet. - */ - public deletePetResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => deletePet(params), - params: () => options, - }); - } - - /** - * Find pet by ID. - * Returns a single pet. - */ - public getPetByIdResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => getPetById(params), - params: () => options, - }); - } - - /** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ - public updatePetWithFormResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => updatePetWithForm(params), - params: () => options, - }); - } - - /** - * Uploads an image. - * Upload image of the pet. - */ - public uploadFileResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => uploadFile(params), - params: () => options, - }); - } -} - -@Injectable({ - providedIn: 'root', -}) -export class StoreServiceResources { - /** - * Returns pet inventories by status. - * Returns a map of status codes to quantities. - */ - public getInventoryResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => getInventory(params), - params: () => options, - }); - } - - /** - * Place an order for a pet. - * Place a new order in the store. - */ - public placeOrderResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => placeOrder(params), - params: () => options, - }); - } - - /** - * Delete purchase order by identifier. - * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. - */ - public deleteOrderResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => deleteOrder(params), - params: () => options, - }); - } - - /** - * Find purchase order by ID. - * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. - */ - public getOrderByIdResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => getOrderById(params), - params: () => options, - }); - } -} - -@Injectable({ - providedIn: 'root', -}) -export class UserServiceResources { - /** - * Create user. - * This can only be done by the logged in user. - */ - public createUserResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => createUser(params), - params: () => options, - }); - } - - /** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ - public createUsersWithListInputResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => createUsersWithListInput(params), - params: () => options, - }); - } - - /** - * Logs user into the system. - * Log into the system. - */ - public loginUserResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => loginUser(params), - params: () => options, - }); - } - - /** - * Logs out current logged in user session. - * Log user out of the system. - */ - public logoutUserResource( - options?: Options, - ) { - return resource({ - loader: async ({ params }) => logoutUser(params), - params: () => options, - }); - } - - /** - * Delete user resource. - * This can only be done by the logged in user. - */ - public deleteUserResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => deleteUser(params), - params: () => options, - }); - } - - /** - * Get user by user name. - * Get user detail based on username. - */ - public getUserByNameResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => getUserByName(params), - params: () => options, - }); - } - - /** - * Update user resource. - * This can only be done by the logged in user. - */ - public updateUserResource( - options: Options, - ) { - return resource({ - loader: async ({ params }) => updateUser(params), - params: () => options, - }); - } -} diff --git a/examples/openapi-ts-angular/src/client/@tanstack/angular-query-experimental.gen.ts b/examples/openapi-ts-angular/src/client/@tanstack/angular-query-experimental.gen.ts deleted file mode 100644 index 890b38180..000000000 --- a/examples/openapi-ts-angular/src/client/@tanstack/angular-query-experimental.gen.ts +++ /dev/null @@ -1,687 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { - type DefaultError, - type MutationOptions, - queryOptions, -} from '@tanstack/angular-query-experimental'; - -import { client as _heyApiClient } from '../client.gen'; -import { - addPet, - createUser, - createUsersWithListInput, - deleteOrder, - deletePet, - deleteUser, - findPetsByStatus, - findPetsByTags, - getInventory, - getOrderById, - getPetById, - getUserByName, - loginUser, - logoutUser, - type Options, - placeOrder, - updatePet, - updatePetWithForm, - updateUser, - uploadFile, -} from '../sdk.gen'; -import type { - AddPetData, - AddPetResponse, - CreateUserData, - CreateUserResponse, - CreateUsersWithListInputData, - CreateUsersWithListInputResponse, - DeleteOrderData, - DeletePetData, - DeleteUserData, - FindPetsByStatusData, - FindPetsByTagsData, - GetInventoryData, - GetOrderByIdData, - GetPetByIdData, - GetUserByNameData, - LoginUserData, - LogoutUserData, - PlaceOrderData, - PlaceOrderResponse, - UpdatePetData, - UpdatePetResponse, - UpdatePetWithFormData, - UpdatePetWithFormResponse, - UpdateUserData, - UploadFileData, - UploadFileResponse, -} from '../types.gen'; - -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - tags?: ReadonlyArray; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, - tags?: ReadonlyArray, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: - options?.baseUrl || - (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (tags) { - params.tags = tags; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - AddPetResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await addPet({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -/** - * Update an existing pet. - * Update an existing pet by Id. - */ -export const updatePetMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - UpdatePetResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await updatePet({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const findPetsByStatusQueryKey = ( - options: Options, -) => createQueryKey('findPetsByStatus', options); - -/** - * Finds Pets by status. - * Multiple status values can be provided with comma separated strings. - */ -export const findPetsByStatusOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await findPetsByStatus({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: findPetsByStatusQueryKey(options), - }); - -export const findPetsByTagsQueryKey = (options: Options) => - createQueryKey('findPetsByTags', options); - -/** - * Finds Pets by tags. - * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - */ -export const findPetsByTagsOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await findPetsByTags({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: findPetsByTagsQueryKey(options), - }); - -/** - * Deletes a pet. - * Delete a pet. - */ -export const deletePetMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - unknown, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await deletePet({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const getPetByIdQueryKey = (options: Options) => - createQueryKey('getPetById', options); - -/** - * Find pet by ID. - * Returns a single pet. - */ -export const getPetByIdOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getPetById({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getPetByIdQueryKey(options), - }); - -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormMutation = ( - options?: Partial>, -): MutationOptions< - UpdatePetWithFormResponse, - DefaultError, - Options -> => { - const mutationOptions: MutationOptions< - UpdatePetWithFormResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await updatePetWithForm({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileMutation = ( - options?: Partial>, -): MutationOptions< - UploadFileResponse, - DefaultError, - Options -> => { - const mutationOptions: MutationOptions< - UploadFileResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await uploadFile({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const getInventoryQueryKey = (options?: Options) => - createQueryKey('getInventory', options); - -/** - * Returns pet inventories by status. - * Returns a map of status codes to quantities. - */ -export const getInventoryOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getInventory({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getInventoryQueryKey(options), - }); - -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderMutation = ( - options?: Partial>, -): MutationOptions< - PlaceOrderResponse, - DefaultError, - Options -> => { - const mutationOptions: MutationOptions< - PlaceOrderResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await placeOrder({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -/** - * Delete purchase order by identifier. - * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. - */ -export const deleteOrderMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - unknown, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await deleteOrder({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const getOrderByIdQueryKey = (options: Options) => - createQueryKey('getOrderById', options); - -/** - * Find purchase order by ID. - * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. - */ -export const getOrderByIdOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getOrderById({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getOrderByIdQueryKey(options), - }); - -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserMutation = ( - options?: Partial>, -): MutationOptions< - CreateUserResponse, - DefaultError, - Options -> => { - const mutationOptions: MutationOptions< - CreateUserResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await createUser({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputMutation = ( - options?: Partial>, -): MutationOptions< - CreateUsersWithListInputResponse, - DefaultError, - Options -> => { - const mutationOptions: MutationOptions< - CreateUsersWithListInputResponse, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await createUsersWithListInput({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const loginUserQueryKey = (options?: Options) => - createQueryKey('loginUser', options); - -/** - * Logs user into the system. - * Log into the system. - */ -export const loginUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await loginUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: loginUserQueryKey(options), - }); - -export const logoutUserQueryKey = (options?: Options) => - createQueryKey('logoutUser', options); - -/** - * Logs out current logged in user session. - * Log user out of the system. - */ -export const logoutUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await logoutUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: logoutUserQueryKey(options), - }); - -/** - * Delete user resource. - * This can only be done by the logged in user. - */ -export const deleteUserMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - unknown, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await deleteUser({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const getUserByNameQueryKey = (options: Options) => - createQueryKey('getUserByName', options); - -/** - * Get user by user name. - * Get user detail based on username. - */ -export const getUserByNameOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getUserByName({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getUserByNameQueryKey(options), - }); - -/** - * Update user resource. - * This can only be done by the logged in user. - */ -export const updateUserMutation = ( - options?: Partial>, -): MutationOptions> => { - const mutationOptions: MutationOptions< - unknown, - DefaultError, - Options - > = { - mutationFn: async (localOptions) => { - const { data } = await updateUser({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; diff --git a/examples/openapi-ts-angular/src/client/client/client.gen.ts b/examples/openapi-ts-angular/src/client/client/client.gen.ts index 9575a92b7..a96dacef0 100644 --- a/examples/openapi-ts-angular/src/client/client/client.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/client.gen.ts @@ -1,11 +1,17 @@ // This file is auto-generated by @hey-api/openapi-ts import type { HttpResponse } from '@angular/common/http'; -import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpEventType, + HttpRequest, +} from '@angular/common/http'; import { assertInInjectionContext, inject, provideAppInitializer, + runInInjectionContext, } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -54,8 +60,14 @@ export const createClient = (config: Config = {}): Client => { }; if (!opts.httpClient) { - assertInInjectionContext(request); - opts.httpClient = inject(HttpClient); + if (opts.injector) { + opts.httpClient = runInInjectionContext(opts.injector, () => + inject(HttpClient), + ); + } else { + assertInInjectionContext(request); + opts.httpClient = inject(HttpClient); + } } if (opts.security) { @@ -111,35 +123,45 @@ export const createClient = (config: Config = {}): Client => { } } - let bodyResponse = response.body as Record; + let bodyResponse: any = response.body; if (opts.responseValidator) { await opts.responseValidator(bodyResponse); } if (opts.responseTransformer) { - bodyResponse = (await opts.responseTransformer(bodyResponse)) as Record< - string, - unknown - >; + bodyResponse = await opts.responseTransformer(bodyResponse); } - return ( - opts.responseStyle === 'data' - ? bodyResponse - : { data: bodyResponse, ...result } - ) as any; + return opts.responseStyle === 'data' + ? bodyResponse + : { data: bodyResponse, ...result }; } catch (error) { + if (error instanceof HttpErrorResponse) { + response = error; + } + + let finalError = error instanceof HttpErrorResponse ? error.error : error; + for (const fn of interceptors.error._fns) { if (fn) { - (await fn(error, response!, req, opts)) as string; + finalError = (await fn( + finalError, + response as HttpResponse, + req, + opts, + )) as string; } } + if (opts.throwOnError) { + throw finalError; + } + return opts.responseStyle === 'data' ? undefined : { - error, + error: finalError, ...result, }; } diff --git a/examples/openapi-ts-angular/src/client/client/types.gen.ts b/examples/openapi-ts-angular/src/client/client/types.gen.ts index b086223a6..e54d1a64e 100644 --- a/examples/openapi-ts-angular/src/client/client/types.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/types.gen.ts @@ -2,9 +2,12 @@ import type { HttpClient, + HttpErrorResponse, + HttpHeaders, HttpRequest, HttpResponse, } from '@angular/common/http'; +import type { Injector } from '@angular/core'; import type { Auth } from '../core/auth.gen'; import type { @@ -17,11 +20,29 @@ export type ResponseStyle = 'data' | 'fields'; export interface Config extends Omit, - CoreConfig { + Omit { /** * Base URL for all requests made by this client. */ baseUrl?: T['baseUrl']; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `HttpHeaders` object with. + * + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} + */ + headers?: + | HttpHeaders + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; /** * The HTTP client to use for making requests. */ @@ -32,6 +53,13 @@ export interface Config * @default 'fields' */ responseStyle?: ResponseStyle; + + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; } export interface RequestOptions< @@ -48,6 +76,10 @@ export interface RequestOptions< * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} */ body?: unknown; + /** + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. + */ + injector?: Injector; path?: Record; query?: Record; /** @@ -80,8 +112,8 @@ export type RequestResult< data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; + request: HttpRequest; + response: HttpResponse; } > : Promise< @@ -91,23 +123,23 @@ export type RequestResult< ? TData[keyof TData] : TData) | undefined - : ( + : | { data: TData extends Record ? TData[keyof TData] : TData; error: undefined; + request: HttpRequest; + response: HttpResponse; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError[keyof TError]; + request: HttpRequest; + response: HttpErrorResponse & { + error: TError[keyof TError] | null; + }; } - ) & { - request: Request; - response: Response; - } >; export interface ClientOptions { diff --git a/examples/openapi-ts-angular/src/client/client/utils.gen.ts b/examples/openapi-ts-angular/src/client/client/utils.gen.ts index d5bf0a1de..9cd7b1faa 100644 --- a/examples/openapi-ts-angular/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/utils.gen.ts @@ -1,5 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts +import { HttpHeaders } from '@angular/common/http'; + import { getAuthToken } from '../core/auth.gen'; import type { QuerySerializer, @@ -198,7 +200,7 @@ export const setAuthParams = async ({ ...options }: Pick, 'security'> & Pick & { - headers: Headers; + headers: HttpHeaders; }) => { for (const auth of security) { const token = await getAuthToken(auth, options.auth); @@ -282,33 +284,47 @@ export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeHeaders = ( ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); +): HttpHeaders => { + let mergedHeaders = new HttpHeaders(); + for (const header of headers) { if (!header || typeof header !== 'object') { continue; } - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); + if (header instanceof HttpHeaders) { + // Merge HttpHeaders instance + header.keys().forEach((key) => { + const values = header.getAll(key); + if (values) { + values.forEach((value) => { + mergedHeaders = mergedHeaders.append(key, value); + }); + } + }); + } else { + // Merge plain object headers + for (const [key, value] of Object.entries(header)) { + if (value === null) { + mergedHeaders = mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders = mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders = mergedHeaders.set( + key, + typeof value === 'object' + ? JSON.stringify(value) + : (value as string), + ); } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); } } } + return mergedHeaders; }; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts b/examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts index 1475ba634..67386b15d 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts @@ -9,7 +9,10 @@ export default defineConfig({ path: './src/client', }, plugins: [ - '@hey-api/client-fetch', + { + name: '@hey-api/client-angular', + // throwOnError: true, + }, '@hey-api/schemas', '@hey-api/sdk', { diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/app.config.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/app.config.ts index 36fe57eee..99ebd5ac2 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/app.config.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/app.config.ts @@ -1,47 +1,24 @@ +import { provideHttpClient, withFetch } from '@angular/common/http'; import type { ApplicationConfig } from '@angular/core'; import { provideZoneChangeDetection } from '@angular/core'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideRouter } from '@angular/router'; import { - provideAngularQuery, + provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental'; import { client } from '../client/client.gen'; +import { provideHeyApiClient } from '../client/client/client.gen'; import { routes } from './app.routes'; -client.setConfig({ - // set default base url for requests made by this client - baseUrl: 'https://petstore3.swagger.io/api/v3', - /** - * Set default headers only for requests made by this client. This is to - * demonstrate local clients and their configuration taking precedence over - * internal service client. - */ - headers: { - Authorization: 'Bearer ', - }, -}); - -client.interceptors.request.use((request, options) => { - // Middleware is great for adding authorization tokens to requests made to - // protected paths. Headers are set randomly here to allow surfacing the - // default headers, too. - if ( - options.url === '/pet/{petId}' && - options.method === 'GET' && - Math.random() < 0.5 - ) { - request.headers.set('Authorization', 'Bearer '); - } - return request; -}); - export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), - provideAngularQuery( + provideHttpClient(withFetch()), + provideHeyApiClient(client), + provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.css b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.css index 8ea9543c6..13b45619a 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.css +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.css @@ -1,16 +1,64 @@ :host { display: grid; gap: 20px; - max-width: 400px; margin: auto; } +.pet-store-header { + display: flex; + gap: 1rem; + align-items: flex-start; + flex-wrap: wrap; +} + mat-card { margin-bottom: 20px; } +.pet-card { + width: 100%; + margin-top: 1rem; +} + +.pet-card-title { + font-size: 1.4rem; + font-weight: 600; +} + +.pet-card-subtitle { + font-size: 1rem; + color: #666; +} + +.pet-card-image { + object-fit: cover; + min-height: 180px; + background: #fafafa; +} + +.pet-status { + margin-top: 0.5rem; + color: #888; +} + +.pet-form-card { + margin-top: 2rem; + max-width: 400px; +} + +.pet-form-title { + font-size: 1.2rem; + font-weight: 500; +} + +.pet-form-content { + display: flex; + flex-direction: column; + gap: 1rem; +} + .actions { display: flex; - gap: 10px; + gap: 0.5rem; } diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.html b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.html index fa2a15a7a..506f36640 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.html +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.html @@ -1,48 +1,63 @@ - - - {{ pet.data()!.name }} - {{ pet.data()!.category }} - - {{ pet.data()!.name }} - +@let pet = petState.data(); +
+ -
-  {{addPet.data()|json}}
-
+ + @if (petState.isLoading()) { + + } -
-  {{updatePet.data()|json}}
-
+ @if (pet) { + + {{ pet.name }} + {{ + pet.category?.name + }} + + + +

+ Status: {{ pet.status }} +

+
+ } +
+
+ +@if (addPet.data()) { +
+    {{addPet.data()|json}}
+  
+} - +@if (updatePet.data()) { +
+    {{updatePet.data()|json}}
+  
+}
- + -

Pet Form

+ Pet Form
- + Name - - - - - Category - + - - + + diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.ts index d6ac696ce..13800796d 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/app/pet-store/pet-store.component.ts @@ -1,11 +1,11 @@ -import { CommonModule } from '@angular/common'; -import { Component, effect, inject, signal } from '@angular/core'; -import type { NgForm } from '@angular/forms'; -import { FormsModule } from '@angular/forms'; +import { JsonPipe } from '@angular/common'; +import { Component, effect, inject, signal, viewChild } from '@angular/core'; +import { FormsModule, NgForm } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatSnackBar } from '@angular/material/snack-bar'; import { injectMutation, @@ -21,12 +21,13 @@ import { @Component({ imports: [ - CommonModule, + JsonPipe, FormsModule, MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule, + MatProgressSpinner, ], selector: 'app-pet-store', standalone: true, @@ -35,83 +36,72 @@ import { }) export class PetStoreComponent { #snackbar = inject(MatSnackBar); + form = viewChild.required(NgForm); - petId = signal(null!); - pet = injectQuery(() => ({ - enabled: this.petId() !== null, + petId = signal(undefined); + + petState = injectQuery(() => ({ + enabled: (this.petId() ?? 0) > 0, ...getPetByIdOptions({ path: { petId: this.petId()! }, }), })); - addPet = injectMutation(() => addPetMutation()); - updatePet = injectMutation(() => updatePetMutation()); + addPet = injectMutation(() => ({ + ...addPetMutation(), + onError: (err) => { + this.#snackbar.open(err.message); + }, + onSuccess: () => { + this.#snackbar.open('Pet added successfully!'); + }, + })); + updatePet = injectMutation(() => ({ + ...updatePetMutation(), + onError: (err) => { + this.#snackbar.open(err.message); + }, + onSuccess: () => { + this.#snackbar.open('Pet updated successfully!'); + }, + })); + + nextPetState: Partial = {}; constructor() { effect(() => { - if (this.pet.isError()) { - this.#snackbar.open(`Pet "${this.petId()}" not found.`); + const err = this.petState.error(); + + if (err) { + this.#snackbar.open(String(err)); } }); - } - // updatePet = useMutation({ - // ...updatePetMutation(), - // onError: (error) => { - // console.log(error); - // }, - // onSuccess: (data) => { - // setPet(data); - // }, - // }); - - // { data, error } = useQuery({ - // ...getPetByIdOptions({ - // client: localClient, - // path: { - // petId: petId!, - // }, - // }), - // enabled: Boolean(petId), - // }); + effect(() => { + this.nextPetState = { ...this.petState.data() }; + }); + } getRandomPet() { // random id 1-10 this.petId.set(Math.floor(Math.random() * (10 - 1 + 1) + 1)); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - handleUpdatePet(name: string, category: string) { - throw new Error('Method not implemented.'); - } + handleUpdatePet = async (event: Event) => { + event.preventDefault(); - onSubmit(form: NgForm) { + await this.updatePet.mutateAsync({ + body: this.nextPetState as Pet, + }); + }; + + onSubmit = async (form: NgForm) => { if (!form.valid) { return; } - const { category, name } = form.value as { - category: string; - name: string; - }; - - this.addPet.mutate({ - body: { - category: { - id: 0, - name: category, - }, - id: 0, - name, - photoUrls: ['string'], - status: 'available', - tags: [ - { - id: 0, - name: 'string', - }, - ], - }, + await this.addPet.mutateAsync({ + body: this.nextPetState as Pet, }); - } + }; } diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts index a64865fe3..890b38180 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts @@ -62,6 +62,7 @@ export type QueryKey = [ Pick & { _id: string; _infinite?: boolean; + tags?: ReadonlyArray; }, ]; @@ -69,14 +70,20 @@ const createQueryKey = ( id: string, options?: TOptions, infinite?: boolean, + tags?: ReadonlyArray, ): [QueryKey[0]] => { const params: QueryKey[0] = { _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, + baseUrl: + options?.baseUrl || + (options?.client ?? _heyApiClient).getConfig().baseUrl, } as QueryKey[0]; if (infinite) { params._infinite = infinite; } + if (tags) { + params.tags = tags; + } if (options?.body) { params.body = options.body; } @@ -162,7 +169,7 @@ export const updatePetMutation = ( }; export const findPetsByStatusQueryKey = ( - options?: Options, + options: Options, ) => createQueryKey('findPetsByStatus', options); /** @@ -170,7 +177,7 @@ export const findPetsByStatusQueryKey = ( * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( - options?: Options, + options: Options, ) => queryOptions({ queryFn: async ({ queryKey, signal }) => { @@ -185,14 +192,14 @@ export const findPetsByStatusOptions = ( queryKey: findPetsByStatusQueryKey(options), }); -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options); /** * Finds Pets by tags. * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts new file mode 100644 index 000000000..3fd4a2427 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts @@ -0,0 +1,190 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { HttpResponse } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpEventType, + HttpRequest, +} from '@angular/common/http'; +import { + assertInInjectionContext, + inject, + provideAppInitializer, + runInInjectionContext, +} from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +export function provideHeyApiClient(client: Client) { + return provideAppInitializer(() => { + const httpClient = inject(HttpClient); + client.setConfig({ httpClient }); + }); +} + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + HttpRequest, + HttpResponse, + unknown, + ResolvedRequestOptions + >(); + + const request: Client['request'] = async (options) => { + const opts = { + ..._config, + ...options, + headers: mergeHeaders(_config.headers, options.headers), + httpClient: options.httpClient ?? _config.httpClient, + serializedBody: options.body as any, + }; + + if (!opts.httpClient) { + if (opts.injector) { + opts.httpClient = runInInjectionContext(opts.injector, () => + inject(HttpClient), + ); + } else { + assertInInjectionContext(request); + opts.httpClient = inject(HttpClient); + } + } + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.serializedBody === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + let req = new HttpRequest( + opts.method, + url, + opts.serializedBody || null, + { + redirect: 'follow', + ...opts, + }, + ); + + for (const fn of interceptors.request._fns) { + if (fn) { + req = await fn(req, opts); + } + } + + let response; + const result = { + request: req, + response, + }; + + try { + response = await firstValueFrom( + opts.httpClient + .request(req) + .pipe(filter((event) => event.type === HttpEventType.Response)), + ); + + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, req, opts); + } + } + + let bodyResponse: any = response.body; + + if (opts.responseValidator) { + await opts.responseValidator(bodyResponse); + } + + if (opts.responseTransformer) { + bodyResponse = await opts.responseTransformer(bodyResponse); + } + + return opts.responseStyle === 'data' + ? bodyResponse + : { data: bodyResponse, ...result }; + } catch (error) { + if (error instanceof HttpErrorResponse) { + response = error; + } + + let finalError = error instanceof HttpErrorResponse ? error.error : error; + + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = (await fn( + finalError, + response as HttpResponse, + req, + opts, + )) as string; + } + } + + if (opts.throwOnError) { + throw finalError; + } + + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + } + }; + + return { + buildUrl, + connect: (options) => request({ ...options, method: 'CONNECT' }), + delete: (options) => request({ ...options, method: 'DELETE' }), + get: (options) => request({ ...options, method: 'GET' }), + getConfig, + head: (options) => request({ ...options, method: 'HEAD' }), + interceptors, + options: (options) => request({ ...options, method: 'OPTIONS' }), + patch: (options) => request({ ...options, method: 'PATCH' }), + post: (options) => request({ ...options, method: 'POST' }), + put: (options) => request({ ...options, method: 'PUT' }), + request, + setConfig, + trace: (options) => request({ ...options, method: 'TRACE' }), + }; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts index 5da1f7aee..318a84b6a 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts @@ -1,12 +1,14 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +18,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts similarity index 73% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts index 75a2ffbbe..e54d1a64e 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts @@ -1,45 +1,59 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpRequest, + HttpResponse, +} from '@angular/common/http'; +import type { Injector } from '@angular/core'; + +import type { Auth } from '../core/auth.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; export interface Config extends Omit, - CoreConfig { + Omit { /** * Base URL for all requests made by this client. */ baseUrl?: T['baseUrl']; /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: (request: Request) => ReturnType; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. + * An object containing any HTTP headers that you want to pre-populate your + * `HttpHeaders` object with. * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} */ - next?: never; + headers?: + | HttpHeaders + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' + * The HTTP client to use for making requests. */ - parseAs?: Exclude | 'auto' | 'stream'; + httpClient?: HttpClient; /** * Should we return only data or multiple fields (data, error, response, etc.)? * * @default 'fields' */ responseStyle?: ResponseStyle; + /** * Throw an error instead of returning it in the response? * @@ -62,6 +76,10 @@ export interface RequestOptions< * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} */ body?: unknown; + /** + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. + */ + injector?: Injector; path?: Record; query?: Record; /** @@ -71,6 +89,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -86,8 +112,8 @@ export type RequestResult< data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; + request: HttpRequest; + response: HttpResponse; } > : Promise< @@ -97,23 +123,23 @@ export type RequestResult< ? TData[keyof TData] : TData) | undefined - : ( + : | { data: TData extends Record ? TData[keyof TData] : TData; error: undefined; + request: HttpRequest; + response: HttpResponse; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError[keyof TError]; + request: HttpRequest; + response: HttpErrorResponse & { + error: TError[keyof TError] | null; + }; } - ) & { - request: Request; - response: Response; - } >; export interface ClientOptions { @@ -153,7 +179,12 @@ type BuildUrlFn = < ) => string; export type Client = CoreClient & { - interceptors: Middleware; + interceptors: Middleware< + HttpRequest, + HttpResponse, + unknown, + ResolvedRequestOptions + >; }; /** diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts similarity index 85% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts index bf3f28250..132dc7ab3 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts @@ -1,15 +1,23 @@ -import { getAuthToken } from '../core/auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import { HttpHeaders } from '@angular/common/http'; + +import { getAuthToken } from '../core/auth.gen'; import type { QuerySerializer, QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +} from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; +} from '../core/pathSerializer.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; interface PathSerializer { path: Record; @@ -147,7 +155,7 @@ export const createQuerySerializer = ({ */ export const getParseAs = ( contentType: string | null, -): Exclude => { +): 'blob' | 'formData' | 'json' | 'stream' | 'text' | undefined => { if (!contentType) { // If no Content-Type header is provided, the best we can do is return the raw response body, // which is effectively the same as the 'stream' option. @@ -182,6 +190,8 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; }; export const setAuthParams = async ({ @@ -189,7 +199,7 @@ export const setAuthParams = async ({ ...options }: Pick, 'security'> & Pick & { - headers: Headers; + headers: HttpHeaders; }) => { for (const auth of security) { const token = await getAuthToken(auth, options.auth); @@ -273,33 +283,47 @@ export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeHeaders = ( ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); +): HttpHeaders => { + let mergedHeaders = new HttpHeaders(); + for (const header of headers) { if (!header || typeof header !== 'object') { continue; } - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); + if (header instanceof HttpHeaders) { + // Merge HttpHeaders instance + header.keys().forEach((key) => { + const values = header.getAll(key); + if (values) { + values.forEach((value) => { + mergedHeaders = mergedHeaders.append(key, value); + }); + } + }); + } else { + // Merge plain object headers + for (const [key, value] of Object.entries(header)) { + if (value === null) { + mergedHeaders = mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders = mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders = mergedHeaders.set( + key, + typeof value === 'object' + ? JSON.stringify(value) + : (value as string), + ); } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); } } } + return mergedHeaders; }; @@ -407,9 +431,7 @@ const defaultHeaders = { export const createConfig = ( override: Config & T> = {}, ): Config & T> => ({ - ...jsonBodySerializer, headers: defaultHeaders, - parseAs: 'auto', querySerializer: defaultQuerySerializer, ...override, }); diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts similarity index 85% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts index fab971b66..9c8e3ec6d 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,7 +16,11 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); } else { @@ -26,7 +32,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +43,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,8 +62,8 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), }; @@ -65,7 +71,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts similarity index 78% rename from examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.ts rename to examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts index 1f8688099..5bfae35c0 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts @@ -1,9 +1,11 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; export interface Client< RequestFn = never, @@ -84,6 +86,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -96,3 +104,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts index f6845bc73..848c7e94c 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts @@ -136,9 +136,9 @@ export const updatePet = ( * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? _heyApiClient).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -158,9 +158,9 @@ export const findPetsByStatus = ( * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? _heyApiClient).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts index ae947e8e4..992c17fb2 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts @@ -136,11 +136,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +169,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +560,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts index f4491fab5..668d15da9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts @@ -1,9 +1,15 @@ import type { HttpResponse } from '@angular/common/http'; -import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpEventType, + HttpRequest, +} from '@angular/common/http'; import { assertInInjectionContext, inject, provideAppInitializer, + runInInjectionContext, } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -48,12 +54,18 @@ export const createClient = (config: Config = {}): Client => { ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: undefined, + serializedBody: options.body as any, }; if (!opts.httpClient) { - assertInInjectionContext(request); - opts.httpClient = inject(HttpClient); + if (opts.injector) { + opts.httpClient = runInInjectionContext(opts.injector, () => + inject(HttpClient), + ); + } else { + assertInInjectionContext(request); + opts.httpClient = inject(HttpClient); + } } if (opts.security) { @@ -78,11 +90,15 @@ export const createClient = (config: Config = {}): Client => { const url = buildUrl(opts); - let req = new HttpRequest(opts.method, url, { - redirect: 'follow', - ...opts, - body: opts.serializedBody, - }); + let req = new HttpRequest( + opts.method, + url, + opts.serializedBody || null, + { + redirect: 'follow', + ...opts, + }, + ); for (const fn of interceptors.request._fns) { if (fn) { @@ -109,35 +125,45 @@ export const createClient = (config: Config = {}): Client => { } } - let bodyResponse = response.body as Record; + let bodyResponse: any = response.body; if (opts.responseValidator) { await opts.responseValidator(bodyResponse); } if (opts.responseTransformer) { - bodyResponse = (await opts.responseTransformer(bodyResponse)) as Record< - string, - unknown - >; + bodyResponse = await opts.responseTransformer(bodyResponse); } - return ( - opts.responseStyle === 'data' - ? bodyResponse - : { data: bodyResponse, ...result } - ) as any; + return opts.responseStyle === 'data' + ? bodyResponse + : { data: bodyResponse, ...result }; } catch (error) { + if (error instanceof HttpErrorResponse) { + response = error; + } + + let finalError = error instanceof HttpErrorResponse ? error.error : error; + for (const fn of interceptors.error._fns) { if (fn) { - (await fn(error, response!, req, opts)) as string; + finalError = (await fn( + finalError, + response as HttpResponse, + req, + opts, + )) as string; } } + if (opts.throwOnError) { + throw finalError; + } + return opts.responseStyle === 'data' ? undefined : { - error, + error: finalError, ...result, }; } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts index 44b0842e9..00467e51b 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts @@ -1,8 +1,11 @@ import type { HttpClient, + HttpErrorResponse, + HttpHeaders, HttpRequest, HttpResponse, } from '@angular/common/http'; +import type { Injector } from '@angular/core'; import type { Auth } from '../../client-core/bundle/auth'; import type { @@ -15,11 +18,29 @@ export type ResponseStyle = 'data' | 'fields'; export interface Config extends Omit, - CoreConfig { + Omit { /** * Base URL for all requests made by this client. */ baseUrl?: T['baseUrl']; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `HttpHeaders` object with. + * + * {@link https://angular.dev/api/common/http/HttpHeaders#constructor See more} + */ + headers?: + | HttpHeaders + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; /** * The HTTP client to use for making requests. */ @@ -30,6 +51,13 @@ export interface Config * @default 'fields' */ responseStyle?: ResponseStyle; + + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; } export interface RequestOptions< @@ -46,6 +74,10 @@ export interface RequestOptions< * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} */ body?: unknown; + /** + * Optional custom injector for dependency resolution if you don't implicitly or explicitly provide one. + */ + injector?: Injector; path?: Record; query?: Record; /** @@ -78,8 +110,8 @@ export type RequestResult< data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; + request: HttpRequest; + response: HttpResponse; } > : Promise< @@ -89,23 +121,23 @@ export type RequestResult< ? TData[keyof TData] : TData) | undefined - : ( + : | { data: TData extends Record ? TData[keyof TData] : TData; error: undefined; + request: HttpRequest; + response: HttpResponse; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError[keyof TError]; + request: HttpRequest; + response: HttpErrorResponse & { + error: TError[keyof TError] | null; + }; } - ) & { - request: Request; - response: Response; - } >; export interface ClientOptions { diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts index d6454394b..cf4d19a20 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/utils.ts @@ -1,9 +1,10 @@ +import { HttpHeaders } from '@angular/common/http'; + import { getAuthToken } from '../../client-core/bundle/auth'; import type { QuerySerializer, QuerySerializerOptions, } from '../../client-core/bundle/bodySerializer'; -import { jsonBodySerializer } from '../../client-core/bundle/bodySerializer'; import { serializeArrayParam, serializeObjectParam, @@ -191,7 +192,7 @@ export const setAuthParams = async ({ ...options }: Pick, 'security'> & Pick & { - headers: Headers; + headers: HttpHeaders; }) => { for (const auth of security) { const token = await getAuthToken(auth, options.auth); @@ -275,33 +276,47 @@ export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeHeaders = ( ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); +): HttpHeaders => { + let mergedHeaders = new HttpHeaders(); + for (const header of headers) { if (!header || typeof header !== 'object') { continue; } - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); + if (header instanceof HttpHeaders) { + // Merge HttpHeaders instance + header.keys().forEach((key) => { + const values = header.getAll(key); + if (values) { + values.forEach((value) => { + mergedHeaders = mergedHeaders.append(key, value); + }); + } + }); + } else { + // Merge plain object headers + for (const [key, value] of Object.entries(header)) { + if (value === null) { + mergedHeaders = mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders = mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders = mergedHeaders.set( + key, + typeof value === 'object' + ? JSON.stringify(value) + : (value as string), + ); } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); } } } + return mergedHeaders; }; @@ -409,9 +424,7 @@ const defaultHeaders = { export const createConfig = ( override: Config & T> = {}, ): Config & T> => ({ - ...jsonBodySerializer, headers: defaultHeaders, - // parseAs: 'auto', querySerializer: defaultQuerySerializer, ...override, });