From f3eaf5a13de07487c10d4c3b5bf7add192c5d3f9 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:11:27 +0800 Subject: [PATCH] hey api orpc plugin --- web/gen/index.ts | 3 + web/gen/orpc.gen.ts | 137 ++++++ web/gen/types.gen.ts | 696 +++++++++++++++++++++++++++++ web/gen/zod.gen.ts | 524 ++++++++++++++++++++++ web/openapi-ts.config.ts | 6 + web/plugins/hey-api-orpc/config.ts | 19 + web/plugins/hey-api-orpc/index.ts | 2 + web/plugins/hey-api-orpc/plugin.ts | 221 +++++++++ web/plugins/hey-api-orpc/types.ts | 36 ++ 9 files changed, 1644 insertions(+) create mode 100644 web/gen/index.ts create mode 100644 web/gen/orpc.gen.ts create mode 100644 web/gen/types.gen.ts create mode 100644 web/gen/zod.gen.ts create mode 100644 web/plugins/hey-api-orpc/config.ts create mode 100644 web/plugins/hey-api-orpc/index.ts create mode 100644 web/plugins/hey-api-orpc/plugin.ts create mode 100644 web/plugins/hey-api-orpc/types.ts diff --git a/web/gen/index.ts b/web/gen/index.ts new file mode 100644 index 0000000000..7b519fe51c --- /dev/null +++ b/web/gen/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { AddPetData, AddPetError, AddPetErrors, AddPetRequest, AddPetResponse, AddPetResponses, Address, ApiResponse, Cat, Category, CatWritable, ClientOptions, CreateUserData, CreateUserResponse, CreateUserResponses, CreateUsersWithListInputData, CreateUsersWithListInputResponse, CreateUsersWithListInputResponses, Customer, DeleteOrderData, DeleteOrderErrors, DeletePetData, DeletePetErrors, DeletePetResponse, DeletePetResponses, DeleteUserData, DeleteUserErrors, Dog, DogWritable, FindPetsByStatusData, FindPetsByStatusErrors, FindPetsByStatusResponse, FindPetsByStatusResponses, FindPetsByTagsData, FindPetsByTagsErrors, FindPetsByTagsResponse, FindPetsByTagsResponses, FullAddress, GetInventoryData, GetInventoryResponse, GetInventoryResponses, GetOrderByIdData, GetOrderByIdErrors, GetOrderByIdResponse, GetOrderByIdResponses, GetPetByIdData, GetPetByIdErrors, GetPetByIdResponse, GetPetByIdResponses, GetUserByNameData, GetUserByNameErrors, GetUserByNameResponse, GetUserByNameResponses, HappyCustomer, LoginUserData, LoginUserErrors, LoginUserResponse, LoginUserResponses, LogoutUserData, LogoutUserResponses, OpenapiCategory, Order, Page, PageSize, Pet, Pet2, PetWritable, PlaceOrderData, PlaceOrderErrors, PlaceOrderPatchData, PlaceOrderPatchErrors, PlaceOrderPatchResponse, PlaceOrderPatchResponses, PlaceOrderResponse, PlaceOrderResponses, Tag, UnhappyCustomer, UpdatePetData, UpdatePetErrors, UpdatePetResponse, UpdatePetResponses, UpdatePetWithFormData, UpdatePetWithFormErrors, UpdateUserData, UpdateUserResponses, UploadFileData, UploadFileResponse, UploadFileResponses, User, UserArray } from './types.gen' diff --git a/web/gen/orpc.gen.ts b/web/gen/orpc.gen.ts new file mode 100644 index 0000000000..e1167ba4eb --- /dev/null +++ b/web/gen/orpc.gen.ts @@ -0,0 +1,137 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { oc } from '@orpc/contract' +import { zAddPetData, zAddPetResponse, zCreateUserData, zCreateUsersWithListInputData, zCreateUsersWithListInputResponse, zDeleteOrderData, zDeletePetData, zDeletePetResponse, zDeleteUserData, zFindPetsByStatusData, zFindPetsByStatusResponse, zFindPetsByTagsData, zFindPetsByTagsResponse, zGetInventoryResponse, zGetOrderByIdData, zGetOrderByIdResponse, zGetPetByIdData, zGetPetByIdResponse, zGetUserByNameData, zGetUserByNameResponse, zLoginUserData, zLoginUserResponse, zPlaceOrderData, zPlaceOrderPatchData, zPlaceOrderPatchResponse, zPlaceOrderResponse, zUpdatePetData, zUpdatePetResponse, zUpdatePetWithFormData, zUpdateUserData, zUploadFileData, zUploadFileResponse } from './zod.gen' + +export const base = oc.$route({ inputStructure: 'detailed' }) + +/** + * Add a new pet to the store + */ +export const addPetContract = base.route({ path: '/pet', method: 'POST' }).input(zAddPetData).output(zAddPetResponse) + +/** + * Update an existing pet by Id + */ +export const updatePetContract = base.route({ path: '/pet', method: 'PUT' }).input(zUpdatePetData).output(zUpdatePetResponse) + +/** + * Multiple status values can be provided with comma separated strings + */ +export const findPetsByStatusContract = base.route({ path: '/pet/findByStatus', method: 'GET' }).input(zFindPetsByStatusData).output(zFindPetsByStatusResponse) + +/** + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + */ +export const findPetsByTagsContract = base.route({ path: '/pet/findByTags', method: 'GET' }).input(zFindPetsByTagsData).output(zFindPetsByTagsResponse) + +/** + * delete a pet + */ +export const deletePetContract = base.route({ path: '/pet/{petId}', method: 'DELETE' }).input(zDeletePetData).output(zDeletePetResponse) + +/** + * Returns a single pet + */ +export const getPetByIdContract = base.route({ path: '/pet/{petId}', method: 'GET' }).input(zGetPetByIdData).output(zGetPetByIdResponse) + +/** + * Updates a pet in the store with form data + */ +export const updatePetWithFormContract = base.route({ path: '/pet/{petId}', method: 'POST' }).input(zUpdatePetWithFormData) + +/** + * uploads an image + */ +export const uploadFileContract = base.route({ path: '/pet/{petId}/uploadImage', method: 'POST' }).input(zUploadFileData).output(zUploadFileResponse) + +/** + * Returns a map of status codes to quantities + */ +export const getInventoryContract = base.route({ path: '/store/inventory', method: 'GET' }).output(zGetInventoryResponse) + +/** + * Place a new order in the store with patch + */ +export const placeOrderPatchContract = base.route({ path: '/store/order', method: 'PATCH' }).input(zPlaceOrderPatchData).output(zPlaceOrderPatchResponse) + +/** + * Place a new order in the store + */ +export const placeOrderContract = base.route({ path: '/store/order', method: 'POST' }).input(zPlaceOrderData).output(zPlaceOrderResponse) + +/** + * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + */ +export const deleteOrderContract = base.route({ path: '/store/order/{orderId}', method: 'DELETE' }).input(zDeleteOrderData) + +/** + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + */ +export const getOrderByIdContract = base.route({ path: '/store/order/{orderId}', method: 'GET' }).input(zGetOrderByIdData).output(zGetOrderByIdResponse) + +/** + * This can only be done by the logged in user. + */ +export const createUserContract = base.route({ path: '/user', method: 'POST' }).input(zCreateUserData) + +/** + * Creates list of users with given input array + */ +export const createUsersWithListInputContract = base.route({ path: '/user/createWithList', method: 'POST' }).input(zCreateUsersWithListInputData).output(zCreateUsersWithListInputResponse) + +/** + * Logs user into the system + */ +export const loginUserContract = base.route({ path: '/user/login', method: 'GET' }).input(zLoginUserData).output(zLoginUserResponse) + +/** + * Logs out current logged in user session + */ +export const logoutUserContract = base.route({ path: '/user/logout', method: 'GET' }) + +/** + * This can only be done by the logged in user. + */ +export const deleteUserContract = base.route({ path: '/user/{username}', method: 'DELETE' }).input(zDeleteUserData) + +/** + * Get user by user name + */ +export const getUserByNameContract = base.route({ path: '/user/{username}', method: 'GET' }).input(zGetUserByNameData).output(zGetUserByNameResponse) + +/** + * This can only be done by the logged in user. + */ +export const updateUserContract = base.route({ path: '/user/{username}', method: 'PUT' }).input(zUpdateUserData) + +export const contracts = { + pet: { + addPetContract, + updatePetContract, + findPetsByStatusContract, + findPetsByTagsContract, + deletePetContract, + getPetByIdContract, + updatePetWithFormContract, + uploadFileContract, + }, + store: { + getInventoryContract, + placeOrderPatchContract, + placeOrderContract, + deleteOrderContract, + getOrderByIdContract, + }, + user: { + createUserContract, + createUsersWithListInputContract, + loginUserContract, + logoutUserContract, + deleteUserContract, + getUserByNameContract, + updateUserContract, + }, +} + +export type Contracts = typeof contracts diff --git a/web/gen/types.gen.ts b/web/gen/types.gen.ts new file mode 100644 index 0000000000..1af881a152 --- /dev/null +++ b/web/gen/types.gen.ts @@ -0,0 +1,696 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}) +} + +export type Order = { + id?: number + petId?: number + quantity?: number + shipDate?: string + /** + * Order Status + */ + status?: '' | string + /** + * HTTP Status + */ + http_status?: 200 | 400 | 500 + complete?: boolean +} + +export type Customer = { + id?: number + username?: string + address?: Array
+} + +export type HappyCustomer = Customer & { + isHappy?: true +} + +export type UnhappyCustomer = Customer & { + reasonToBeUnhappy?: string + isHappy?: false +} + +export type Address = { + streetName?: string + streetNumber?: string + city?: string + state?: string + zip?: string +} + +export type Category = { + id?: number + name?: string +} + +export type User = { + id?: number + username?: string + firstName?: string + lastName?: string + email?: string + password?: string + phone?: string + /** + * User Status + */ + userStatus?: number +} + +export type Tag = { + id?: number + name?: string +} + +export type Pet = (({ + type: 'dog' +} & Dog) | ({ + type: 'cat' +} & Cat)) & { + id?: number + type: 'dog' | 'cat' + name: string + category?: OpenapiCategory + photoUrls: Array + readonly tags?: Array + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold' +} + +export type Cat = { + readonly type?: string + name?: string +} + +export type Dog = { + readonly type?: string + bark?: string +} + +export type FullAddress = Address & { + streetName: string + streetNumber: string +} + +export type AddPetRequest = { + id?: number + name: string + category?: Category + photoUrls: Array + tags?: Array + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold' | 'in store' +} + +export type ApiResponse = { + code?: number + type?: string + message?: string +} + +export type OpenapiCategory = { + id?: number + name?: string +} + +export type PetWritable = (({ + type: 'DogWritable' +} & DogWritable) | ({ + type: 'CatWritable' +} & CatWritable)) & { + id?: number + name: string + category?: OpenapiCategory + photoUrls: Array + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold' +} + +export type CatWritable = { + name?: string +} + +export type DogWritable = { + bark?: string +} + +/** + * to request with required page number or pagination + */ +export type Page = string + +/** + * to request with required page size + */ +export type PageSize = string + +/** + * Pet object that needs to be added to the store + */ +export type Pet2 = PetWritable + +/** + * List of user object + */ +export type UserArray = Array + +export type AddPetData = { + /** + * Create a new pet in the store + */ + body: AddPetRequest + path?: never + query?: never + url: '/pet' +} + +export type AddPetErrors = { + /** + * Pet not found + */ + 405: { + code?: number + message?: string + } +} + +export type AddPetError = AddPetErrors[keyof AddPetErrors] + +export type AddPetResponses = { + /** + * Successful operation + */ + 200: Pet +} + +export type AddPetResponse = AddPetResponses[keyof AddPetResponses] + +export type UpdatePetData = { + /** + * Update an existent pet in the store + */ + body: PetWritable + path?: never + query?: never + url: '/pet' +} + +export type UpdatePetErrors = { + /** + * Invalid ID supplied + */ + 400: unknown + /** + * Pet not found + */ + 404: unknown + /** + * Validation exception + */ + 405: unknown +} + +export type UpdatePetResponses = { + /** + * Successful operation + */ + 200: Pet +} + +export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses] + +export type FindPetsByStatusData = { + body?: never + path?: never + query?: { + /** + * Status values that need to be considered for filter + */ + status?: 'available' | 'pending' | 'sold' + } + url: '/pet/findByStatus' +} + +export type FindPetsByStatusErrors = { + /** + * Invalid status value + */ + 400: unknown +} + +export type FindPetsByStatusResponses = { + /** + * successful operation + */ + 200: Array +} + +export type FindPetsByStatusResponse = FindPetsByStatusResponses[keyof FindPetsByStatusResponses] + +export type FindPetsByTagsData = { + body?: never + path?: never + query?: { + /** + * Tags to filter by + */ + tags?: Array + /** + * to request with required page number or pagination + */ + page?: string + /** + * to request with required page size + */ + pageSize?: string + } + url: '/pet/findByTags' +} + +export type FindPetsByTagsErrors = { + /** + * Invalid tag value + */ + 400: unknown +} + +export type FindPetsByTagsResponses = { + /** + * successful operation + */ + 200: Array +} + +export type FindPetsByTagsResponse = FindPetsByTagsResponses[keyof FindPetsByTagsResponses] + +export type DeletePetData = { + body?: never + headers?: { + api_key?: string + } + path: { + /** + * Pet id to delete + */ + petId: number + } + query?: never + url: '/pet/{petId}' +} + +export type DeletePetErrors = { + /** + * Invalid pet value + */ + 400: unknown +} + +export type DeletePetResponses = { + /** + * items + */ + 200: Array<'TYPE1' | 'TYPE2' | 'TYPE3'> +} + +export type DeletePetResponse = DeletePetResponses[keyof DeletePetResponses] + +export type GetPetByIdData = { + body?: never + path: { + /** + * ID of pet to return + */ + petId: number + } + query?: never + url: '/pet/{petId}' +} + +export type GetPetByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown + /** + * Pet not found + */ + 404: unknown +} + +export type GetPetByIdResponses = { + /** + * successful operation + */ + 200: Pet +} + +export type GetPetByIdResponse = GetPetByIdResponses[keyof GetPetByIdResponses] + +export type UpdatePetWithFormData = { + body?: never + path: { + /** + * ID of pet that needs to be updated + */ + petId: number + } + query?: { + /** + * Name of pet that needs to be updated + */ + name?: string + /** + * Status of pet that needs to be updated + */ + status?: string + } + url: '/pet/{petId}' +} + +export type UpdatePetWithFormErrors = { + /** + * Invalid input + */ + 405: unknown +} + +export type UploadFileData = { + body?: Blob | File + path: { + /** + * ID of pet to update + */ + petId: number + } + query?: { + /** + * Additional Metadata + */ + additionalMetadata?: string + } + url: '/pet/{petId}/uploadImage' +} + +export type UploadFileResponses = { + /** + * successful operation + */ + 200: ApiResponse +} + +export type UploadFileResponse = UploadFileResponses[keyof UploadFileResponses] + +export type GetInventoryData = { + body?: never + path?: never + query?: never + url: '/store/inventory' +} + +export type GetInventoryResponses = { + /** + * successful operation + */ + 200: { + [key: string]: number + } +} + +export type GetInventoryResponse = GetInventoryResponses[keyof GetInventoryResponses] + +export type PlaceOrderPatchData = { + body?: Order + path?: never + query?: never + url: '/store/order' +} + +export type PlaceOrderPatchErrors = { + /** + * Invalid input + */ + 405: unknown +} + +export type PlaceOrderPatchResponses = { + /** + * successful operation + */ + 200: Order +} + +export type PlaceOrderPatchResponse = PlaceOrderPatchResponses[keyof PlaceOrderPatchResponses] + +export type PlaceOrderData = { + /** + * Order description + */ + body?: Order + path?: never + query?: never + url: '/store/order' +} + +export type PlaceOrderErrors = { + /** + * Invalid input + */ + 405: unknown +} + +export type PlaceOrderResponses = { + /** + * successful operation + */ + 200: Order +} + +export type PlaceOrderResponse = PlaceOrderResponses[keyof PlaceOrderResponses] + +export type DeleteOrderData = { + body?: never + path: { + /** + * ID of the order that needs to be deleted + */ + orderId: number + } + query?: never + url: '/store/order/{orderId}' +} + +export type DeleteOrderErrors = { + /** + * Invalid ID supplied + */ + 400: unknown + /** + * Order not found + */ + 404: unknown +} + +export type GetOrderByIdData = { + body?: never + path: { + /** + * ID of order that needs to be fetched + */ + orderId: number + } + query?: never + url: '/store/order/{orderId}' +} + +export type GetOrderByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown + /** + * Order not found + */ + 404: unknown +} + +export type GetOrderByIdResponses = { + /** + * successful operation + */ + 200: Order +} + +export type GetOrderByIdResponse = GetOrderByIdResponses[keyof GetOrderByIdResponses] + +export type CreateUserData = { + /** + * Created user object + */ + body?: User + path?: never + query?: never + url: '/user' +} + +export type CreateUserResponses = { + /** + * successful operation + */ + default: User +} + +export type CreateUserResponse = CreateUserResponses[keyof CreateUserResponses] + +export type CreateUsersWithListInputData = { + body?: Array + path?: never + query?: never + url: '/user/createWithList' +} + +export type CreateUsersWithListInputResponses = { + /** + * Successful operation + */ + 200: User + /** + * successful operation + */ + default: unknown +} + +export type CreateUsersWithListInputResponse = CreateUsersWithListInputResponses[keyof CreateUsersWithListInputResponses] + +export type LoginUserData = { + body?: never + path?: never + query?: { + /** + * The user name for login + */ + username?: string + /** + * The password for login in clear text + */ + password?: string + } + url: '/user/login' +} + +export type LoginUserErrors = { + /** + * Invalid username/password supplied + */ + 400: unknown +} + +export type LoginUserResponses = { + /** + * successful operation + */ + 200: string +} + +export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses] + +export type LogoutUserData = { + body?: never + path?: never + query?: never + url: '/user/logout' +} + +export type LogoutUserResponses = { + /** + * successful operation + */ + default: unknown +} + +export type DeleteUserData = { + body?: never + path: { + /** + * The name that needs to be deleted + */ + username: string + } + query?: never + url: '/user/{username}' +} + +export type DeleteUserErrors = { + /** + * Invalid username supplied + */ + 400: unknown + /** + * User not found + */ + 404: unknown +} + +export type GetUserByNameData = { + body?: never + path: { + /** + * The name that needs to be fetched. Use user1 for testing. + */ + username: string + } + query?: never + url: '/user/{username}' +} + +export type GetUserByNameErrors = { + /** + * Invalid username supplied + */ + 400: unknown + /** + * User not found + */ + 404: unknown +} + +export type GetUserByNameResponses = { + /** + * successful operation + */ + 200: User +} + +export type GetUserByNameResponse = GetUserByNameResponses[keyof GetUserByNameResponses] + +export type UpdateUserData = { + /** + * Update an existent user in the store + */ + body?: User + path: { + /** + * name that need to be deleted + */ + username: string + } + query?: never + url: '/user/{username}' +} + +export type UpdateUserResponses = { + /** + * successful operation + */ + default: unknown +} diff --git a/web/gen/zod.gen.ts b/web/gen/zod.gen.ts new file mode 100644 index 0000000000..8198a96619 --- /dev/null +++ b/web/gen/zod.gen.ts @@ -0,0 +1,524 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod' + +export const zOrder = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + petId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + quantity: z.number().int().min(-2147483648, { message: 'Invalid value: Expected int32 to be >= -2147483648' }).max(2147483647, { message: 'Invalid value: Expected int32 to be <= 2147483647' }).optional(), + shipDate: z.string().datetime().optional(), + status: z.union([ + z.literal(''), + z.string().email(), + ]).optional(), + http_status: z.union([ + z.literal(200), + z.literal(400), + z.literal(500), + ]).describe('HTTP Status').optional(), + complete: z.boolean().optional(), +}) + +export type OrderZodType = z.infer + +export const zAddress = z.object({ + streetName: z.string().optional(), + streetNumber: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zip: z.string().optional(), +}) + +export type AddressZodType = z.infer + +export const zCustomer = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + username: z.string().optional(), + address: z.array(zAddress).optional(), +}) + +export type CustomerZodType = z.infer + +export const zHappyCustomer = zCustomer.and(z.object({ + isHappy: z.literal(true).optional(), +})) + +export type HappyCustomerZodType = z.infer + +export const zUnhappyCustomer = zCustomer.and(z.object({ + reasonToBeUnhappy: z.string().optional(), + isHappy: z.literal(false).optional(), +})) + +export type UnhappyCustomerZodType = z.infer + +export const zCategory = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + name: z.string().optional(), +}) + +export type CategoryZodType = z.infer + +export const zUser = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + username: z.string().optional(), + firstName: z.string().optional(), + lastName: z.string().optional(), + email: z.string().optional(), + password: z.string().optional(), + phone: z.string().optional(), + userStatus: z.number().int().min(-2147483648, { message: 'Invalid value: Expected int32 to be >= -2147483648' }).max(2147483647, { message: 'Invalid value: Expected int32 to be <= 2147483647' }).describe('User Status').optional(), +}) + +export type UserZodType = z.infer + +export const zTag = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + name: z.string().optional(), +}) + +export type TagZodType = z.infer + +export const zCat = z.object({ + type: z.string().min(1).readonly().optional(), + name: z.string().optional(), +}) + +export type CatZodType = z.infer + +export const zDog = z.object({ + type: z.string().min(1).readonly().optional(), + bark: z.string().optional(), +}) + +export type DogZodType = z.infer + +export const zFullAddress = zAddress.and(z.object({ + streetName: z.string(), + streetNumber: z.string(), +})) + +export type FullAddressZodType = z.infer + +export const zAddPetRequest = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + name: z.string(), + category: zCategory.optional(), + photoUrls: z.array(z.string()), + tags: z.array(zTag).optional(), + status: z.enum([ + 'available', + 'pending', + 'sold', + 'in store', + ]).describe('pet status in the store').optional(), +}) + +export type AddPetRequestZodType = z.infer + +export const zApiResponse = z.object({ + code: z.number().int().min(-2147483648, { message: 'Invalid value: Expected int32 to be >= -2147483648' }).max(2147483647, { message: 'Invalid value: Expected int32 to be <= 2147483647' }).optional(), + type: z.string().optional(), + message: z.string().optional(), +}) + +export type ApiResponseZodType = z.infer + +export const zOpenapiCategory = z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + name: z.string().optional(), +}) + +export type OpenapiCategoryZodType = z.infer + +export const zPet = z.intersection(z.union([ + z.object({ + type: z.literal('dog'), + }).and(zDog), + z.object({ + type: z.literal('cat'), + }).and(zCat), +]), z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + type: z.enum(['dog', 'cat']), + name: z.string(), + category: zOpenapiCategory.optional(), + photoUrls: z.array(z.string()), + tags: z.array(zTag).readonly().optional(), + status: z.enum([ + 'available', + 'pending', + 'sold', + ]).describe('pet status in the store').optional(), +})) + +export type PetZodType = z.infer + +export const zCatWritable = z.object({ + name: z.string().optional(), +}) + +export type CatWritableZodType = z.infer + +export const zDogWritable = z.object({ + bark: z.string().optional(), +}) + +export type DogWritableZodType = z.infer + +export const zPetWritable = z.intersection(z.union([ + z.object({ + type: z.literal('DogWritable'), + }).and(zDogWritable), + z.object({ + type: z.literal('CatWritable'), + }).and(zCatWritable), +]), z.object({ + id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), + name: z.string(), + category: zOpenapiCategory.optional(), + photoUrls: z.array(z.string()), + status: z.enum([ + 'available', + 'pending', + 'sold', + ]).describe('pet status in the store').optional(), +})) + +export type PetWritableZodType = z.infer + +/** + * to request with required page number or pagination + */ +export const zPage = z.string().describe('to request with required page number or pagination') + +export type PageZodType = z.infer + +/** + * to request with required page size + */ +export const zPageSize = z.string().describe('to request with required page size') + +export type PageSizeZodType = z.infer + +/** + * Pet object that needs to be added to the store + */ +export const zPet2 = zPetWritable + +export type PetZodType2 = z.infer + +/** + * List of user object + */ +export const zUserArray = z.array(zUser).describe('List of user object') + +export type UserArrayZodType = z.infer + +export const zAddPetData = z.object({ + body: zAddPetRequest, + path: z.never().optional(), + query: z.never().optional(), +}) + +export type AddPetDataZodType = z.infer + +/** + * Successful operation + */ +export const zAddPetResponse = zPet + +export type AddPetResponseZodType = z.infer + +export const zUpdatePetData = z.object({ + body: zPetWritable, + path: z.never().optional(), + query: z.never().optional(), +}) + +export type UpdatePetDataZodType = z.infer + +/** + * Successful operation + */ +export const zUpdatePetResponse = zPet + +export type UpdatePetResponseZodType = z.infer + +export const zFindPetsByStatusData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.object({ + status: z.enum([ + 'available', + 'pending', + 'sold', + ]).describe('Status values that need to be considered for filter').optional(), + }).optional(), +}) + +export type FindPetsByStatusDataZodType = z.infer + +/** + * successful operation + */ +export const zFindPetsByStatusResponse = z.array(zPet).describe('successful operation') + +export type FindPetsByStatusResponseZodType = z.infer + +export const zFindPetsByTagsData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.object({ + tags: z.array(z.string()).describe('Tags to filter by').optional(), + page: z.string().describe('to request with required page number or pagination').optional(), + pageSize: z.string().describe('to request with required page size').optional(), + }).optional(), +}) + +export type FindPetsByTagsDataZodType = z.infer + +/** + * successful operation + */ +export const zFindPetsByTagsResponse = z.array(zPet).describe('successful operation') + +export type FindPetsByTagsResponseZodType = z.infer + +export const zDeletePetData = z.object({ + body: z.never().optional(), + path: z.object({ + petId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('Pet id to delete'), + }), + query: z.never().optional(), + headers: z.object({ + api_key: z.string().optional(), + }).optional(), +}) + +export type DeletePetDataZodType = z.infer + +/** + * items + */ +export const zDeletePetResponse = z.array(z.enum([ + 'TYPE1', + 'TYPE2', + 'TYPE3', +])).describe('items') + +export type DeletePetResponseZodType = z.infer + +export const zGetPetByIdData = z.object({ + body: z.never().optional(), + path: z.object({ + petId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('ID of pet to return'), + }), + query: z.never().optional(), +}) + +export type GetPetByIdDataZodType = z.infer + +/** + * successful operation + */ +export const zGetPetByIdResponse = zPet + +export type GetPetByIdResponseZodType = z.infer + +export const zUpdatePetWithFormData = z.object({ + body: z.never().optional(), + path: z.object({ + petId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('ID of pet that needs to be updated'), + }), + query: z.object({ + name: z.string().describe('Name of pet that needs to be updated').optional(), + status: z.string().describe('Status of pet that needs to be updated').optional(), + }).optional(), +}) + +export type UpdatePetWithFormDataZodType = z.infer + +export const zUploadFileData = z.object({ + body: z.string().optional(), + path: z.object({ + petId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('ID of pet to update'), + }), + query: z.object({ + additionalMetadata: z.string().describe('Additional Metadata').optional(), + }).optional(), +}) + +export type UploadFileDataZodType = z.infer + +/** + * successful operation + */ +export const zUploadFileResponse = zApiResponse + +export type UploadFileResponseZodType = z.infer + +export const zGetInventoryData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type GetInventoryDataZodType = z.infer + +/** + * successful operation + */ +export const zGetInventoryResponse = z.record(z.number().int().min(-2147483648, { message: 'Invalid value: Expected int32 to be >= -2147483648' }).max(2147483647, { message: 'Invalid value: Expected int32 to be <= 2147483647' })).describe('successful operation') + +export type GetInventoryResponseZodType = z.infer + +export const zPlaceOrderPatchData = z.object({ + body: zOrder.optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type PlaceOrderPatchDataZodType = z.infer + +/** + * successful operation + */ +export const zPlaceOrderPatchResponse = zOrder + +export type PlaceOrderPatchResponseZodType = z.infer + +export const zPlaceOrderData = z.object({ + body: zOrder.optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type PlaceOrderDataZodType = z.infer + +/** + * successful operation + */ +export const zPlaceOrderResponse = zOrder + +export type PlaceOrderResponseZodType = z.infer + +export const zDeleteOrderData = z.object({ + body: z.never().optional(), + path: z.object({ + orderId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('ID of the order that needs to be deleted'), + }), + query: z.never().optional(), +}) + +export type DeleteOrderDataZodType = z.infer + +export const zGetOrderByIdData = z.object({ + body: z.never().optional(), + path: z.object({ + orderId: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).describe('ID of order that needs to be fetched'), + }), + query: z.never().optional(), +}) + +export type GetOrderByIdDataZodType = z.infer + +/** + * successful operation + */ +export const zGetOrderByIdResponse = zOrder + +export type GetOrderByIdResponseZodType = z.infer + +export const zCreateUserData = z.object({ + body: zUser.optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type CreateUserDataZodType = z.infer + +/** + * successful operation + */ +export const zCreateUserResponse = zUser + +export type CreateUserResponseZodType = z.infer + +export const zCreateUsersWithListInputData = z.object({ + body: z.array(zUser).optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type CreateUsersWithListInputDataZodType = z.infer + +export const zCreateUsersWithListInputResponse = z.union([ + zUser, + z.unknown().describe('successful operation'), +]) + +export type CreateUsersWithListInputResponseZodType = z.infer + +export const zLoginUserData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.object({ + username: z.string().describe('The user name for login').optional(), + password: z.string().describe('The password for login in clear text').optional(), + }).optional(), +}) + +export type LoginUserDataZodType = z.infer + +/** + * successful operation + */ +export const zLoginUserResponse = z.string().describe('successful operation') + +export type LoginUserResponseZodType = z.infer + +export const zLogoutUserData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional(), +}) + +export type LogoutUserDataZodType = z.infer + +export const zDeleteUserData = z.object({ + body: z.never().optional(), + path: z.object({ + username: z.string().describe('The name that needs to be deleted'), + }), + query: z.never().optional(), +}) + +export type DeleteUserDataZodType = z.infer + +export const zGetUserByNameData = z.object({ + body: z.never().optional(), + path: z.object({ + username: z.string().describe('The name that needs to be fetched. Use user1 for testing. '), + }), + query: z.never().optional(), +}) + +export type GetUserByNameDataZodType = z.infer + +/** + * successful operation + */ +export const zGetUserByNameResponse = zUser + +export type GetUserByNameResponseZodType = z.infer + +export const zUpdateUserData = z.object({ + body: zUser.optional(), + path: z.object({ + username: z.string().describe('name that need to be deleted'), + }), + query: z.never().optional(), +}) + +export type UpdateUserDataZodType = z.infer diff --git a/web/openapi-ts.config.ts b/web/openapi-ts.config.ts index 3aaaa2daee..6641db0b0e 100644 --- a/web/openapi-ts.config.ts +++ b/web/openapi-ts.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from '@hey-api/openapi-ts' +import { defineConfig as defineOrpcConfig } from './plugins/hey-api-orpc' + export default defineConfig({ input: './open-api/petStore.yaml', output: './gen', @@ -15,5 +17,9 @@ export default defineConfig({ infer: true, }, }, + defineOrpcConfig({ + output: 'orpc', + generateRouter: true, + }), ], }) diff --git a/web/plugins/hey-api-orpc/config.ts b/web/plugins/hey-api-orpc/config.ts new file mode 100644 index 0000000000..1cda602e9d --- /dev/null +++ b/web/plugins/hey-api-orpc/config.ts @@ -0,0 +1,19 @@ +import type { OrpcPlugin } from './types' + +import { definePluginConfig } from '@hey-api/openapi-ts' + +import { handler } from './plugin' + +export const defaultConfig: OrpcPlugin['Config'] = { + config: { + baseName: 'base', + exportFromIndex: false, + generateRouter: true, + output: 'orpc', + }, + dependencies: ['@hey-api/typescript', 'zod'], + handler, + name: 'orpc', +} + +export const defineConfig = definePluginConfig(defaultConfig) diff --git a/web/plugins/hey-api-orpc/index.ts b/web/plugins/hey-api-orpc/index.ts new file mode 100644 index 0000000000..f2f65bcd22 --- /dev/null +++ b/web/plugins/hey-api-orpc/index.ts @@ -0,0 +1,2 @@ +export { defaultConfig, defineConfig } from './config' +export type { Config, OrpcPlugin, ResolvedConfig } from './types' diff --git a/web/plugins/hey-api-orpc/plugin.ts b/web/plugins/hey-api-orpc/plugin.ts new file mode 100644 index 0000000000..1713778800 --- /dev/null +++ b/web/plugins/hey-api-orpc/plugin.ts @@ -0,0 +1,221 @@ +import type { IR } from '@hey-api/openapi-ts' +import type { OrpcPlugin } from './types' + +import { $ } from '@hey-api/openapi-ts' + +function capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +function toZodSchemaName(operationId: string, type: 'data' | 'response'): string { + const pascalName = capitalizeFirst(operationId) + return type === 'data' ? `z${pascalName}Data` : `z${pascalName}Response` +} + +type OperationInfo = { + id: string + method: string + path: string + description?: string + deprecated?: boolean + tags: string[] + hasInput: boolean + hasOutput: boolean + zodDataSchema: string + zodResponseSchema: string +} + +function collectOperation(operation: IR.OperationObject): OperationInfo { + const id = operation.id || `${operation.method}_${operation.path.replace(/[{}/]/g, '_')}` + + const hasPathParams = Boolean(operation.parameters?.path && Object.keys(operation.parameters.path).length > 0) + const hasQueryParams = Boolean(operation.parameters?.query && Object.keys(operation.parameters.query).length > 0) + const hasBody = Boolean(operation.body) + const hasInput = hasPathParams || hasQueryParams || hasBody + + // Check if operation has a successful response with actual content + // Look for 2xx responses that have a schema with mediaType (indicating response body) + let hasOutput = false + if (operation.responses) { + for (const [statusCode, response] of Object.entries(operation.responses)) { + // Check for 2xx success responses with actual content + if (statusCode.startsWith('2') && response?.mediaType && response?.schema) { + hasOutput = true + break + } + } + } + + return { + deprecated: operation.deprecated, + description: operation.description || operation.summary, + hasInput, + hasOutput, + id, + method: operation.method.toUpperCase(), + path: operation.path, + tags: operation.tags ? [...operation.tags] : ['default'], + zodDataSchema: toZodSchemaName(id, 'data'), + zodResponseSchema: toZodSchemaName(id, 'response'), + } +} + +export const handler: OrpcPlugin['Handler'] = ({ plugin }) => { + const config = plugin.config + const operations: OperationInfo[] = [] + const zodImports = new Set() + + // Collect all operations using hey-api's forEach + plugin.forEach('operation', (event) => { + const info = collectOperation(event.operation) + operations.push(info) + + // Collect zod imports + if (info.hasInput) { + zodImports.add(info.zodDataSchema) + } + if (info.hasOutput) { + zodImports.add(info.zodResponseSchema) + } + }) + + // Register external symbols for imports + const symbolOc = plugin.symbol('oc', { + exported: false, + external: '@orpc/contract', + }) + + // Register zod schema symbols (they come from zod plugin) + const zodSchemaSymbols: Record> = {} + for (const schemaName of zodImports) { + zodSchemaSymbols[schemaName] = plugin.symbol(schemaName, { + exported: false, + external: './zod.gen', + }) + } + + // Create base contract: export const base = oc.$route({ inputStructure: 'detailed' }) + const baseSymbol = plugin.symbol(config.baseName, { + exported: true, + meta: { + category: 'schema', + }, + }) + + const baseNode = $.const(baseSymbol) + .export() + .assign( + $(symbolOc) + .attr('$route') + .call( + $.object() + .prop('inputStructure', $.literal('detailed')), + ), + ) + plugin.node(baseNode) + + // Create contract for each operation + // Store symbols for later use in contracts object + const contractSymbols: Record> = {} + + for (const op of operations) { + const contractSymbol = plugin.symbol(`${op.id}Contract`, { + exported: true, + meta: { + category: 'schema', + }, + }) + contractSymbols[op.id] = contractSymbol + + // Build the call chain: base.route({...}).input(...).output(...) + let expression = $(baseSymbol) + .attr('route') + .call( + $.object() + .prop('path', $.literal(op.path)) + .prop('method', $.literal(op.method)), + ) + + // .input(zodDataSchema) if has input + if (op.hasInput) { + expression = expression + .attr('input') + .call($(zodSchemaSymbols[op.zodDataSchema])) + } + + // .output(zodResponseSchema) if has output + if (op.hasOutput) { + expression = expression + .attr('output') + .call($(zodSchemaSymbols[op.zodResponseSchema])) + } + + const contractNode = $.const(contractSymbol) + .export() + .$if(op.description || op.deprecated, (node) => { + const docLines: string[] = [] + if (op.description) { + docLines.push(op.description) + } + if (op.deprecated) { + docLines.push('@deprecated') + } + return node.doc(docLines) + }) + .assign(expression) + + plugin.node(contractNode) + } + + // Create contracts object export if enabled + if (config.generateRouter) { + // Group operations by tag + const operationsByTag = new Map() + for (const op of operations) { + const tag = op.tags[0] + if (!operationsByTag.has(tag)) { + operationsByTag.set(tag, []) + } + operationsByTag.get(tag)!.push(op) + } + + // Build contracts object + const contractsObject = $.object() + for (const [tag, tagOps] of operationsByTag) { + const tagKey = tag.charAt(0).toLowerCase() + tag.slice(1) + const tagObject = $.object() + for (const op of tagOps) { + const contractSymbol = contractSymbols[op.id] + if (contractSymbol) { + tagObject.prop(`${op.id}Contract`, $(contractSymbol)) + } + } + contractsObject.prop(tagKey, tagObject) + } + + const contractsSymbol = plugin.symbol('contracts', { + exported: true, + meta: { + category: 'schema', + }, + }) + + const contractsNode = $.const(contractsSymbol) + .export() + .assign(contractsObject) + plugin.node(contractsNode) + + // Create type export: export type Contracts = typeof contracts + const contractsTypeSymbol = plugin.symbol('Contracts', { + exported: true, + meta: { + category: 'type', + }, + }) + + const contractsTypeNode = $.type.alias(contractsTypeSymbol) + .export() + .type($.type.query($(contractsSymbol))) + plugin.node(contractsTypeNode) + } +} diff --git a/web/plugins/hey-api-orpc/types.ts b/web/plugins/hey-api-orpc/types.ts new file mode 100644 index 0000000000..b9bdcbf653 --- /dev/null +++ b/web/plugins/hey-api-orpc/types.ts @@ -0,0 +1,36 @@ +import type { DefinePlugin } from '@hey-api/openapi-ts' + +export type Config = { name: 'orpc' } & { + /** + * Name of the generated file. + * @default 'orpc' + */ + output?: string + + /** + * The name of the base contract variable. + * @default 'base' + */ + baseName?: string + + /** + * Whether to generate a contracts object that combines all contracts. + * @default true + */ + generateRouter?: boolean + + /** + * Whether to export from index file. + * @default false + */ + exportFromIndex?: boolean +} + +export type ResolvedConfig = Config & { + output: string + baseName: string + generateRouter: boolean + exportFromIndex: boolean +} + +export type OrpcPlugin = DefinePlugin