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

Configure Feed

Select the types of activity you want to include in your feed.

chore: polish validator option

Lubos 014e038e 8e1e62db

+428 -371
+1
dev/package.json
··· 13 13 "@hey-api/openapi-python": "workspace:*", 14 14 "@hey-api/openapi-ts": "workspace:*", 15 15 "@opencode-ai/sdk": "1.2.27", 16 + "@orpc/contract": "1.13.4", 16 17 "@pinia/colada": "0.19.1", 17 18 "@tanstack/angular-query-experimental": "5.90.25", 18 19 "@tanstack/preact-query": "5.93.0",
+7
dev/typescript/presets.ts
··· 30 30 }, 31 31 }, 32 32 ], 33 + rpc: () => [ 34 + /** RPC-style SDK with Zod validation */ 35 + { 36 + name: '@orpc/contract', 37 + }, 38 + 'zod', 39 + ], 33 40 sdk: () => [ 34 41 /** SDK with types */ 35 42 '@hey-api/typescript',
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.0.x/custom-contract-name/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersRpc = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserRpc = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserRpc = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdRpc = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserRpc = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsRpc = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostRpc = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdRpc = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.0.x/custom-router-name/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersContract = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserContract = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserContract = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdContract = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserContract = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsContract = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostContract = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdContract = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.0.x/default/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersContract = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserContract = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserContract = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdContract = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserContract = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsContract = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostContract = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdContract = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.1.x/custom-contract-name/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersRpc = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserRpc = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserRpc = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdRpc = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserRpc = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsRpc = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostRpc = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdRpc = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.1.x/custom-router-name/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersContract = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserContract = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserContract = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdContract = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserContract = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsContract = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostContract = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdContract = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+12 -12
packages/openapi-ts-tests/@orpc/contract/v1/__snapshots__/3.1.x/default/@orpc/contract.gen.ts
··· 11 11 */ 12 12 export const getUsersContract = base.route({ 13 13 method: 'GET', 14 - path: '/users', 15 14 operationId: 'getUsers', 15 + path: '/users', 16 16 summary: 'Get all users', 17 17 tags: ['users'] 18 18 }).input(zGetUsersData).output(zGetUsersResponse); ··· 22 22 */ 23 23 export const createUserContract = base.route({ 24 24 method: 'POST', 25 - path: '/users', 26 25 operationId: 'createUser', 26 + path: '/users', 27 + successStatus: 201, 27 28 summary: 'Create a new user', 28 - tags: ['users'], 29 - successStatus: 201 29 + tags: ['users'] 30 30 }).input(zCreateUserData).output(zCreateUserResponse); 31 31 32 32 /** ··· 34 34 */ 35 35 export const deleteUserContract = base.route({ 36 36 method: 'DELETE', 37 - path: '/users/{userId}', 38 37 operationId: 'deleteUser', 38 + path: '/users/{userId}', 39 39 summary: 'Delete a user', 40 40 tags: ['users'] 41 41 }).input(zDeleteUserData); ··· 45 45 */ 46 46 export const getUserByIdContract = base.route({ 47 47 method: 'GET', 48 - path: '/users/{userId}', 49 48 operationId: 'getUserById', 49 + path: '/users/{userId}', 50 50 summary: 'Get a user by ID', 51 51 tags: ['users'] 52 52 }).input(zGetUserByIdData).output(zGetUserByIdResponse); ··· 56 56 */ 57 57 export const updateUserContract = base.route({ 58 58 method: 'PUT', 59 - path: '/users/{userId}', 60 59 operationId: 'updateUser', 60 + path: '/users/{userId}', 61 61 summary: 'Update a user', 62 62 tags: ['users'] 63 63 }).input(zUpdateUserData).output(zUpdateUserResponse); ··· 67 67 */ 68 68 export const getPostsContract = base.route({ 69 69 method: 'GET', 70 - path: '/posts', 71 70 operationId: 'getPosts', 71 + path: '/posts', 72 72 summary: 'Get all posts', 73 73 tags: ['posts'] 74 74 }).input(zGetPostsData).output(zGetPostsResponse); ··· 78 78 */ 79 79 export const createPostContract = base.route({ 80 80 method: 'POST', 81 - path: '/posts', 82 81 operationId: 'createPost', 82 + path: '/posts', 83 + successStatus: 201, 83 84 summary: 'Create a new post', 84 - tags: ['posts'], 85 - successStatus: 201 85 + tags: ['posts'] 86 86 }).input(zCreatePostData).output(zCreatePostResponse); 87 87 88 88 /** ··· 90 90 */ 91 91 export const getPostByIdContract = base.route({ 92 92 method: 'GET', 93 - path: '/posts/{postId}', 94 93 operationId: 'getPostById', 94 + path: '/posts/{postId}', 95 95 summary: 'Get a post by ID', 96 96 tags: ['posts'] 97 97 }).input(zGetPostByIdData).output(zGetPostByIdResponse);
+1
packages/openapi-ts-tests/@orpc/contract/v1/test/3.0.x.test.ts
··· 19 19 config: createConfig({ 20 20 input: 'orpc-contract.yaml', 21 21 output: 'default', 22 + plugins: ['@orpc/contract', 'zod'], 22 23 }), 23 24 description: 'generate oRPC contracts with Zod schemas', 24 25 },
+1
packages/openapi-ts-tests/@orpc/contract/v1/test/3.1.x.test.ts
··· 19 19 config: createConfig({ 20 20 input: 'orpc-contract.yaml', 21 21 output: 'default', 22 + plugins: ['@orpc/contract', 'zod'], 22 23 }), 23 24 description: 'generate oRPC contracts with Zod schemas', 24 25 },
+1 -1
packages/openapi-ts-tests/@orpc/contract/v1/test/utils.ts
··· 16 16 const output = userConfig.output instanceof Array ? userConfig.output[0]! : userConfig.output; 17 17 const outputPath = typeof output === 'string' ? output : (output?.path ?? ''); 18 18 return { 19 - plugins: ['zod', '@orpc/contract'], 19 + plugins: ['@orpc/contract'], 20 20 ...userConfig, 21 21 input: 22 22 typeof userConfig.input === 'string'
+55 -6
packages/openapi-ts/src/plugins/@orpc/contract/config.ts
··· 1 + import { log } from '@hey-api/codegen-core'; 1 2 import type { OperationsStrategy, PluginContext } from '@hey-api/shared'; 2 3 import { definePluginConfig, resolveNaming } from '@hey-api/shared'; 3 4 4 5 import { handler } from './plugin'; 5 6 import type { OrpcContractPlugin, RouterConfig, UserRouterConfig } from './types'; 7 + 8 + const validatorInferWarn = 9 + 'You set `validator: true` but no validator plugin was found in your plugins. Add a validator plugin like `zod` to enable this feature. The validator option has been disabled.'; 6 10 7 11 function resolveRouter( 8 12 input: OperationsStrategy | UserRouterConfig | undefined, ··· 49 53 export const defaultConfig: OrpcContractPlugin['Config'] = { 50 54 config: { 51 55 contractNameBuilder: (id: string) => `${id}Contract`, 52 - exportFromIndex: false, 56 + includeInEntry: false, 53 57 router: { 54 58 methodName: { casing: 'camelCase' }, 55 59 nesting: 'operationId', ··· 59 63 strategyDefaultTag: 'default', 60 64 }, 61 65 routerName: { name: 'router' }, 62 - validator: 'zod', 63 66 }, 64 67 handler, 65 68 name: '@orpc/contract', 66 69 resolveConfig: (plugin, context) => { 67 - plugin.config.exportFromIndex ??= false; 68 70 plugin.config.contractNameBuilder ??= (id: string) => `${id}Contract`; 69 71 plugin.config.router = resolveRouter(plugin.config.router, context); 70 72 plugin.config.routerName = resolveNaming(plugin.config.routerName); ··· 72 74 plugin.config.routerName.name = 'router'; 73 75 } 74 76 75 - plugin.config.validator ??= 'zod'; 76 - plugin.dependencies.add(plugin.config.validator); 77 + if (typeof plugin.config.validator !== 'object') { 78 + plugin.config.validator = { 79 + input: plugin.config.validator, 80 + output: plugin.config.validator, 81 + }; 82 + } 83 + 84 + if (plugin.config.validator.input || plugin.config.validator.input === undefined) { 85 + if ( 86 + typeof plugin.config.validator.input === 'boolean' || 87 + plugin.config.validator.input === undefined 88 + ) { 89 + try { 90 + plugin.config.validator.input = context.pluginByTag('validator'); 91 + plugin.dependencies.add(plugin.config.validator.input!); 92 + } catch { 93 + // avoid showing the warning with default configuration as it would be confusing 94 + if (plugin.config.validator.input !== undefined) { 95 + log.warn(validatorInferWarn); 96 + } 97 + plugin.config.validator.input = false; 98 + } 99 + } else { 100 + plugin.dependencies.add(plugin.config.validator.input); 101 + } 102 + } else { 103 + plugin.config.validator.input = false; 104 + } 105 + 106 + if (plugin.config.validator.output || plugin.config.validator.output === undefined) { 107 + if ( 108 + typeof plugin.config.validator.output === 'boolean' || 109 + plugin.config.validator.output === undefined 110 + ) { 111 + try { 112 + plugin.config.validator.output = context.pluginByTag('validator'); 113 + plugin.dependencies.add(plugin.config.validator.output!); 114 + } catch { 115 + // avoid showing the warning with default configuration as it would be confusing 116 + if (plugin.config.validator.output !== undefined) { 117 + log.warn(validatorInferWarn); 118 + } 119 + plugin.config.validator.output = false; 120 + } 121 + } else { 122 + plugin.dependencies.add(plugin.config.validator.output); 123 + } 124 + } else { 125 + plugin.config.validator.output = false; 126 + } 77 127 }, 78 - tags: ['client'], 79 128 }; 80 129 81 130 /**
+3 -271
packages/openapi-ts/src/plugins/@orpc/contract/plugin.ts
··· 1 - import type { NodeName } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 3 - import { applyNaming, OperationPath, OperationStrategy, toCase } from '@hey-api/shared'; 4 - 5 - import { $ } from '../../../ts-dsl'; 6 - import { createOperationComment } from '../../shared/utils/operation'; 7 - import type { OrpcContractPlugin, RouterConfig } from './types'; 8 - 9 - function hasInput(operation: IR.OperationObject): boolean { 10 - const hasPathParams = Boolean( 11 - operation.parameters?.path && Object.keys(operation.parameters.path).length > 0, 12 - ); 13 - const hasQueryParams = Boolean( 14 - operation.parameters?.query && Object.keys(operation.parameters.query).length > 0, 15 - ); 16 - const hasHeaderParams = Boolean( 17 - operation.parameters?.header && Object.keys(operation.parameters.header).length > 0, 18 - ); 19 - const hasBody = Boolean(operation.body); 20 - return hasPathParams || hasQueryParams || hasHeaderParams || hasBody; 21 - } 22 - 23 - function getSuccessResponse( 24 - operation: IR.OperationObject, 25 - ): { hasOutput: true; statusCode: number } | { hasOutput: false; statusCode?: undefined } { 26 - if (operation.responses) { 27 - for (const [statusCode, response] of Object.entries(operation.responses)) { 28 - const statusCodeNumber = Number.parseInt(statusCode, 10); 29 - if ( 30 - statusCodeNumber >= 200 && 31 - statusCodeNumber <= 399 && 32 - response?.mediaType && 33 - response?.schema 34 - ) { 35 - return { hasOutput: true, statusCode: statusCodeNumber }; 36 - } 37 - } 38 - } 39 - return { hasOutput: false, statusCode: undefined }; 40 - } 41 - 42 - function getTags(operation: IR.OperationObject, defaultTag: string): string[] { 43 - return operation.tags && operation.tags.length > 0 ? [...operation.tags] : [defaultTag]; 44 - } 45 - 46 - function getOperationPaths( 47 - operation: IR.OperationObject, 48 - routerConfig: RouterConfig, 49 - ): ReadonlyArray<ReadonlyArray<string>> { 50 - const { nesting, nestingDelimiters, strategy, strategyDefaultTag } = routerConfig; 51 - 52 - // Get path derivation function 53 - let pathFn = OperationPath.id(); 54 - if (typeof nesting === 'function') { 55 - pathFn = nesting; 56 - } else if (nesting === 'operationId') { 57 - pathFn = OperationPath.fromOperationId({ delimiters: nestingDelimiters }); 58 - } 59 - 60 - // Get structure strategy function 61 - let strategyFn; 62 - if (typeof strategy === 'function') { 63 - strategyFn = strategy; 64 - } else if (strategy === 'byTags') { 65 - strategyFn = OperationStrategy.byTags({ 66 - fallback: strategyDefaultTag, 67 - path: pathFn, 68 - }); 69 - } else if (strategy === 'single') { 70 - strategyFn = OperationStrategy.single({ 71 - path: pathFn, 72 - root: strategyDefaultTag, 73 - }); 74 - } else { 75 - // flat 76 - strategyFn = OperationStrategy.flat({ path: pathFn }); 77 - } 78 - 79 - return strategyFn(operation); 80 - } 81 - 82 - type NestedLeaf = { type: 'leaf'; value: NodeName }; 83 - type NestedNode = { children: Map<string, NestedValue>; type: 'node' }; 84 - type NestedValue = NestedLeaf | NestedNode; 85 - 86 - function buildNestedObject(node: NestedNode): ReturnType<typeof $.object> { 87 - const obj = $.object(); 88 - for (const [key, child] of node.children) { 89 - if (child.type === 'leaf') { 90 - obj.prop(key, $(child.value)); 91 - } else { 92 - obj.prop(key, buildNestedObject(child)); 93 - } 94 - } 95 - return obj; 96 - } 97 - 98 - export const handler: OrpcContractPlugin['Handler'] = ({ plugin }) => { 99 - const { contractNameBuilder, router, routerName, validator } = plugin.config; 100 - 101 - const operations: IR.OperationObject[] = []; 102 - 103 - plugin.forEach( 104 - 'operation', 105 - (event) => { 106 - operations.push(event.operation); 107 - }, 108 - { order: 'declarations' }, 109 - ); 110 - 111 - const symbolOc = plugin.symbol('oc', { 112 - exported: false, 113 - external: '@orpc/contract', 114 - }); 115 - 116 - const baseSymbol = plugin.symbol('base', { 117 - exported: true, 118 - meta: { 119 - category: 'contract', 120 - resource: 'base', 121 - tool: '@orpc/contract', 122 - }, 123 - }); 124 - 125 - const baseNode = $.const(baseSymbol) 126 - .export() 127 - .assign( 128 - $(symbolOc) 129 - .attr('$route') 130 - .call($.object().prop('inputStructure', $.literal('detailed'))), 131 - ); 132 - plugin.node(baseNode); 133 - 134 - const contractSymbols: Record<string, ReturnType<typeof plugin.symbol>> = {}; 135 - 136 - for (const op of operations) { 137 - const contractName = contractNameBuilder(op.id); 138 - const tags = getTags(op, router.strategyDefaultTag); 139 - const successResponse = getSuccessResponse(op); 1 + import type { OrpcContractPlugin } from './types'; 2 + import { handlerV1 } from './v1/plugin'; 140 3 141 - const contractSymbol = plugin.symbol(contractName, { 142 - exported: true, 143 - meta: { 144 - category: 'contract', 145 - path: ['paths', op.path, op.method], 146 - resource: 'operation', 147 - resourceId: op.id, 148 - role: 'contract', 149 - tags, 150 - tool: '@orpc/contract', 151 - }, 152 - }); 153 - contractSymbols[op.id] = contractSymbol; 154 - 155 - const method = op.method.toUpperCase(); 156 - const routeConfig = $.object() 157 - .prop('method', $.literal(method)) 158 - .prop('path', $.literal(op.path as string)) 159 - .$if(op.operationId, (node) => node.prop('operationId', $.literal(op.operationId!))) 160 - .$if(op.summary, (node) => node.prop('summary', $.literal(op.summary!))) 161 - .$if(op.description, (node) => node.prop('description', $.literal(op.description!))) 162 - .$if(op.deprecated, (node) => node.prop('deprecated', $.literal(true))) 163 - .$if(tags.length > 0, (node) => node.prop('tags', $.fromValue(tags))) 164 - .$if(successResponse.hasOutput && successResponse.statusCode !== 200, (node) => 165 - node.prop('successStatus', $.literal(successResponse.statusCode!)), 166 - ); 167 - 168 - let expression = $(baseSymbol).attr('route').call(routeConfig); 169 - 170 - if (hasInput(op)) { 171 - const dataSymbol = plugin.referenceSymbol({ 172 - category: 'schema', 173 - resource: 'operation', 174 - resourceId: op.id, 175 - role: 'data', 176 - tool: validator, 177 - }); 178 - if (dataSymbol) { 179 - expression = expression.attr('input').call($(dataSymbol)); 180 - } 181 - } 182 - 183 - if (successResponse.hasOutput) { 184 - // TODO: support outputStructure detailed 185 - const responseSymbol = plugin.referenceSymbol({ 186 - category: 'schema', 187 - resource: 'operation', 188 - resourceId: op.id, 189 - role: 'responses', 190 - tool: validator, 191 - }); 192 - if (responseSymbol) { 193 - expression = expression.attr('output').call($(responseSymbol)); 194 - } 195 - } 196 - 197 - const comments = createOperationComment(op); 198 - const contractNode = $.const(contractSymbol) 199 - .export() 200 - .$if(comments, (node) => node.doc(comments)) 201 - .assign(expression); 202 - 203 - plugin.node(contractNode); 204 - } 205 - 206 - const routerExportName = applyNaming('router', routerName); 207 - const contractsSymbol = plugin.symbol(routerExportName, { 208 - exported: true, 209 - meta: { 210 - category: 'contract', 211 - resource: 'router', 212 - tool: '@orpc/contract', 213 - }, 214 - }); 215 - 216 - // Build nested structure using a tree 217 - const root: NestedNode = { children: new Map(), type: 'node' }; 218 - 219 - for (const op of operations) { 220 - const contractSymbol = contractSymbols[op.id]; 221 - if (contractSymbol) { 222 - const paths = getOperationPaths(op, router); 223 - for (const path of paths) { 224 - let current: NestedNode = root; 225 - for (let i = 0; i < path.length; i++) { 226 - const isLast = i === path.length - 1; 227 - const segment = isLast 228 - ? applyNaming(path[i]!, router.methodName) 229 - : applyNaming(path[i]!, router.segmentName); 230 - 231 - if (isLast) { 232 - current.children.set(segment, { 233 - type: 'leaf', 234 - value: contractSymbol, 235 - }); 236 - } else { 237 - if (!current.children.has(segment)) { 238 - current.children.set(segment, { 239 - children: new Map(), 240 - type: 'node', 241 - }); 242 - } 243 - const next = current.children.get(segment)!; 244 - if (next.type === 'node') { 245 - current = next; 246 - } 247 - } 248 - } 249 - } 250 - } 251 - } 252 - 253 - const contractsObject = buildNestedObject(root).pretty(); 254 - const contractsNode = $.const(contractsSymbol).export().assign(contractsObject); 255 - plugin.node(contractsNode); 256 - 257 - const routerTypeName = toCase(routerExportName, 'PascalCase'); 258 - const routerTypeSymbol = plugin.symbol(routerTypeName, { 259 - exported: true, 260 - meta: { 261 - category: 'type', 262 - resource: 'router', 263 - tool: '@orpc/contract', 264 - }, 265 - }); 266 - 267 - const routerTypeNode = $.type 268 - .alias(routerTypeSymbol) 269 - .export() 270 - .type($.type.query($(contractsSymbol))); 271 - plugin.node(routerTypeNode); 272 - }; 4 + export const handler: OrpcContractPlugin['Handler'] = (args) => handlerV1(args);
+77
packages/openapi-ts/src/plugins/@orpc/contract/shared/operation.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + import { OperationPath, OperationStrategy } from '@hey-api/shared'; 3 + 4 + import type { RouterConfig } from '../types'; 5 + 6 + export function hasInput(operation: IR.OperationObject): boolean { 7 + const hasPathParams = Boolean( 8 + operation.parameters?.path && Object.keys(operation.parameters.path).length > 0, 9 + ); 10 + const hasQueryParams = Boolean( 11 + operation.parameters?.query && Object.keys(operation.parameters.query).length > 0, 12 + ); 13 + const hasHeaderParams = Boolean( 14 + operation.parameters?.header && Object.keys(operation.parameters.header).length > 0, 15 + ); 16 + const hasBody = Boolean(operation.body); 17 + return hasPathParams || hasQueryParams || hasHeaderParams || hasBody; 18 + } 19 + 20 + export function getSuccessResponse( 21 + operation: IR.OperationObject, 22 + ): { hasOutput: true; statusCode: number } | { hasOutput: false; statusCode?: undefined } { 23 + if (operation.responses) { 24 + for (const [statusCode, response] of Object.entries(operation.responses)) { 25 + const statusCodeNumber = Number.parseInt(statusCode, 10); 26 + if ( 27 + statusCodeNumber >= 200 && 28 + statusCodeNumber <= 399 && 29 + response?.mediaType && 30 + response?.schema 31 + ) { 32 + return { hasOutput: true, statusCode: statusCodeNumber }; 33 + } 34 + } 35 + } 36 + return { hasOutput: false, statusCode: undefined }; 37 + } 38 + 39 + export function getTags(operation: IR.OperationObject, defaultTag: string): ReadonlyArray<string> { 40 + return operation.tags && operation.tags.length > 0 ? [...operation.tags] : [defaultTag]; 41 + } 42 + 43 + export function getOperationPaths( 44 + operation: IR.OperationObject, 45 + routerConfig: RouterConfig, 46 + ): ReadonlyArray<ReadonlyArray<string>> { 47 + const { nesting, nestingDelimiters, strategy, strategyDefaultTag } = routerConfig; 48 + 49 + // Get path derivation function 50 + let pathFn = OperationPath.id(); 51 + if (typeof nesting === 'function') { 52 + pathFn = nesting; 53 + } else if (nesting === 'operationId') { 54 + pathFn = OperationPath.fromOperationId({ delimiters: nestingDelimiters }); 55 + } 56 + 57 + // Get structure strategy function 58 + let strategyFn; 59 + if (typeof strategy === 'function') { 60 + strategyFn = strategy; 61 + } else if (strategy === 'byTags') { 62 + strategyFn = OperationStrategy.byTags({ 63 + fallback: strategyDefaultTag, 64 + path: pathFn, 65 + }); 66 + } else if (strategy === 'single') { 67 + strategyFn = OperationStrategy.single({ 68 + path: pathFn, 69 + root: strategyDefaultTag, 70 + }); 71 + } else { 72 + // flat 73 + strategyFn = OperationStrategy.flat({ path: pathFn }); 74 + } 75 + 76 + return strategyFn(operation); 77 + }
+36 -17
packages/openapi-ts/src/plugins/@orpc/contract/types.d.ts
··· 111 111 } 112 112 113 113 export type UserConfig = Plugin.Name<'@orpc/contract'> & 114 - Plugin.Hooks & { 114 + Plugin.Hooks & 115 + Plugin.UserExports & { 115 116 /** 116 117 * Custom naming function for contract symbols. 117 118 * 118 119 * @default (id) => `${id}Contract` 119 120 */ 120 121 contractNameBuilder?: (operationId: string) => string; 121 - /** 122 - * Whether exports should be re-exported in the index file. 123 - * 124 - * @default false 125 - */ 126 - exportFromIndex?: boolean; 127 122 /** 128 123 * Router configuration for grouping and nesting operations. 129 124 * ··· 165 160 */ 166 161 routerName?: NamingRule; 167 162 /** 168 - * Validator plugin to use for input/output schemas. 163 + * Validate input/output schemas. 169 164 * 170 - * Ensure you have declared the selected library as a dependency to avoid 171 - * errors. 172 - * 173 - * @default 'zod' 165 + * @default true 174 166 */ 175 - validator?: PluginValidatorNames; 167 + validator?: 168 + | PluginValidatorNames 169 + | boolean 170 + | { 171 + /** 172 + * The validator plugin to use for input schemas. 173 + * 174 + * Can be a validator plugin name or boolean (true to auto-select, false 175 + * to disable). 176 + * 177 + * @default true 178 + */ 179 + input?: PluginValidatorNames | boolean; 180 + /** 181 + * The validator plugin to use for output schemas. 182 + * 183 + * Can be a validator plugin name or boolean (true to auto-select, false 184 + * to disable). 185 + * 186 + * @default true 187 + */ 188 + output?: PluginValidatorNames | boolean; 189 + }; 176 190 }; 177 191 178 192 export type Config = Plugin.Name<'@orpc/contract'> & 179 - Plugin.Hooks & { 193 + Plugin.Hooks & 194 + Plugin.Exports & { 180 195 contractNameBuilder: (operationId: string) => string; 181 - exportFromIndex: boolean; 182 - output: string; 183 196 router: RouterConfig; 184 197 routerName: NamingConfig; 185 - validator: PluginValidatorNames; 198 + /** Validate input/output schemas. */ 199 + validator: { 200 + /** The validator plugin to use for input schemas. */ 201 + input: PluginValidatorNames | false; 202 + /** The validator plugin to use for output schemas. */ 203 + output: PluginValidatorNames | false; 204 + }; 186 205 }; 187 206 188 207 export type OrpcContractPlugin = DefinePlugin<UserConfig, Config>;
+166
packages/openapi-ts/src/plugins/@orpc/contract/v1/plugin.ts
··· 1 + import type { NodeName } from '@hey-api/codegen-core'; 2 + import { applyNaming, toCase } from '@hey-api/shared'; 3 + 4 + import { $ } from '../../../../ts-dsl'; 5 + import { createOperationComment } from '../../../shared/utils/operation'; 6 + import { getOperationPaths, getSuccessResponse, getTags, hasInput } from '../shared/operation'; 7 + import type { OrpcContractPlugin } from '../types'; 8 + 9 + type NestedLeaf = { type: 'leaf'; value: NodeName }; 10 + type NestedNode = { children: Map<string, NestedValue>; type: 'node' }; 11 + type NestedValue = NestedLeaf | NestedNode; 12 + 13 + function buildNestedObject(node: NestedNode): ReturnType<typeof $.object> { 14 + const obj = $.object(); 15 + for (const [key, child] of node.children) { 16 + if (child.type === 'leaf') { 17 + obj.prop(key, $(child.value)); 18 + } else { 19 + obj.prop(key, buildNestedObject(child)); 20 + } 21 + } 22 + return obj; 23 + } 24 + 25 + export const handlerV1: OrpcContractPlugin['Handler'] = ({ plugin }) => { 26 + const oc = plugin.symbol('oc', { 27 + external: '@orpc/contract', 28 + }); 29 + const baseSymbol = plugin.symbol('base'); 30 + 31 + const baseNode = $.const(baseSymbol) 32 + .export() 33 + .assign( 34 + $(oc) 35 + .attr('$route') 36 + .call($.object().prop('inputStructure', $.literal('detailed'))), 37 + ); 38 + plugin.node(baseNode); 39 + 40 + const root: NestedNode = { children: new Map(), type: 'node' }; 41 + 42 + plugin.forEach( 43 + 'operation', 44 + (event) => { 45 + const { operation } = event; 46 + 47 + const contractName = plugin.config.contractNameBuilder(operation.id); 48 + const tags = getTags(operation, plugin.config.router.strategyDefaultTag); 49 + const successResponse = getSuccessResponse(operation); 50 + 51 + const contractSymbol = plugin.symbol(contractName, { 52 + meta: { 53 + category: 'contract', 54 + path: event._path, 55 + resource: 'operation', 56 + resourceId: operation.id, 57 + role: 'contract', 58 + tags, 59 + tool: plugin.name, 60 + }, 61 + }); 62 + 63 + let expression = $(baseSymbol) 64 + .attr('route') 65 + .call( 66 + $.object() 67 + .$if(operation.deprecated, (o, v) => o.prop('deprecated', $.literal(v))) 68 + .$if(operation.description, (o, v) => o.prop('description', $.literal(v))) 69 + .prop('method', $.literal(operation.method.toUpperCase())) 70 + .$if(operation.operationId, (o, v) => o.prop('operationId', $.literal(v))) 71 + .prop('path', $.literal(operation.path)) 72 + .$if( 73 + successResponse.hasOutput && 74 + successResponse.statusCode !== 200 && 75 + successResponse.statusCode, 76 + (o, v) => o.prop('successStatus', $.literal(v)), 77 + ) 78 + .$if(operation.summary, (o, v) => o.prop('summary', $.literal(v))) 79 + .$if(tags.length > 0 && tags, (o, v) => o.prop('tags', $.fromValue(v))), 80 + ); 81 + 82 + if (hasInput(operation) && plugin.config.validator.input) { 83 + expression = expression.attr('input').call( 84 + plugin.referenceSymbol({ 85 + category: 'schema', 86 + resource: 'operation', 87 + resourceId: operation.id, 88 + role: 'data', 89 + tool: plugin.config.validator.input, 90 + }), 91 + ); 92 + } 93 + 94 + if (successResponse.hasOutput && plugin.config.validator.output) { 95 + // TODO: support outputStructure detailed 96 + expression = expression.attr('output').call( 97 + plugin.referenceSymbol({ 98 + category: 'schema', 99 + resource: 'operation', 100 + resourceId: operation.id, 101 + role: 'responses', 102 + tool: plugin.config.validator.output, 103 + }), 104 + ); 105 + } 106 + 107 + const contractNode = $.const(contractSymbol) 108 + .export() 109 + .$if(createOperationComment(operation), (c, v) => c.doc(v)) 110 + .assign(expression); 111 + plugin.node(contractNode); 112 + 113 + const paths = getOperationPaths(operation, plugin.config.router); 114 + for (const path of paths) { 115 + let current: NestedNode = root; 116 + for (let i = 0; i < path.length; i++) { 117 + const isLast = i === path.length - 1; 118 + const segment = isLast 119 + ? applyNaming(path[i]!, plugin.config.router.methodName) 120 + : applyNaming(path[i]!, plugin.config.router.segmentName); 121 + 122 + if (isLast) { 123 + current.children.set(segment, { 124 + type: 'leaf', 125 + value: contractSymbol, 126 + }); 127 + } else { 128 + if (!current.children.has(segment)) { 129 + current.children.set(segment, { 130 + children: new Map(), 131 + type: 'node', 132 + }); 133 + } 134 + const next = current.children.get(segment)!; 135 + if (next.type === 'node') { 136 + current = next; 137 + } 138 + } 139 + } 140 + } 141 + }, 142 + { order: 'declarations' }, 143 + ); 144 + 145 + const routerExportName = applyNaming('router', plugin.config.routerName); 146 + const routerSymbol = plugin.symbol(routerExportName, { 147 + meta: { 148 + category: 'contract', 149 + resource: 'router', 150 + tool: plugin.name, 151 + }, 152 + }); 153 + const routerNode = $.const(routerSymbol).export().assign(buildNestedObject(root).pretty()); 154 + plugin.node(routerNode); 155 + 156 + const routerTypeName = toCase(routerExportName, 'PascalCase'); 157 + const routerTypeSymbol = plugin.symbol(routerTypeName, { 158 + meta: { 159 + category: 'type', 160 + resource: 'router', 161 + tool: plugin.name, 162 + }, 163 + }); 164 + const routerTypeNode = $.type.alias(routerTypeSymbol).export().type($.type.query(routerSymbol)); 165 + plugin.node(routerTypeNode); 166 + };
+5 -4
packages/openapi-ts/src/ts-dsl/type/query.ts
··· 1 - import type { AnalysisContext, NodeScope } from '@hey-api/codegen-core'; 1 + import type { AnalysisContext, NodeName, NodeScope, Ref } from '@hey-api/codegen-core'; 2 + import { ref } from '@hey-api/codegen-core'; 2 3 import ts from 'typescript'; 3 4 4 5 import type { MaybeTsDsl, TypeTsDsl } from '../base'; ··· 6 7 import { TypeExprMixin } from '../mixins/type-expr'; 7 8 import { f } from '../utils/factories'; 8 9 9 - export type TypeQueryExpr = string | MaybeTsDsl<TypeTsDsl | ts.Expression>; 10 + export type TypeQueryExpr = NodeName | MaybeTsDsl<TypeTsDsl | ts.Expression>; 10 11 export type TypeQueryCtor = (expr: TypeQueryExpr) => TypeQueryTsDsl; 11 12 12 13 const Mixed = TypeExprMixin(TsDsl<ts.TypeQueryNode>); ··· 15 16 readonly '~dsl' = 'TypeQueryTsDsl'; 16 17 override scope: NodeScope = 'type'; 17 18 18 - protected _expr: TypeQueryExpr; 19 + protected _expr: Ref<TypeQueryExpr>; 19 20 20 21 constructor(expr: TypeQueryExpr) { 21 22 super(); 22 - this._expr = expr; 23 + this._expr = ref(expr); 23 24 } 24 25 25 26 override analyze(ctx: AnalysisContext): void {
+3
pnpm-lock.yaml
··· 118 118 '@opencode-ai/sdk': 119 119 specifier: 1.2.27 120 120 version: 1.2.27 121 + '@orpc/contract': 122 + specifier: 1.13.4 123 + version: 1.13.4 121 124 '@pinia/colada': 122 125 specifier: 0.19.1 123 126 version: 0.19.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))