diff --git a/web/gen/orpc/router.ts b/web/gen/orpc/router.ts index 37a52583e6..c531304584 100644 --- a/web/gen/orpc/router.ts +++ b/web/gen/orpc/router.ts @@ -15,13 +15,13 @@ import { textToAudioChatContract } from './api/text-to-audio' export const router = { chatMessages: { send: sendChatMessageContract, stopGeneration: stopChatMessageGenerationContract }, - files: { upload: uploadChatFileContract, preview: previewChatFileContract }, + files: { uploadChat: uploadChatFileContract, previewChat: previewChatFileContract }, messages: { - postFeedback: postChatMessageFeedbackContract, + postChatFeedback: postChatMessageFeedbackContract, getSuggestedQuestions: getSuggestedQuestionsContract, getConversationHistory: getConversationHistoryContract, }, - app: { getFeedbacks: getChatAppFeedbacksContract }, + app: { getChatFeedbacks: getChatAppFeedbacksContract }, conversations: { getList: getConversationsListContract, delete: deleteConversationContract, @@ -29,11 +29,11 @@ export const router = { getVariables: getConversationVariablesContract, }, audioToText: { audioToText: audioToTextContract }, - textToAudio: { textToAudio: textToAudioChatContract }, - info: { get: getChatAppInfoContract }, - parameters: { get: getChatAppParametersContract }, - meta: { get: getChatAppMetaContract }, - site: { getSettings: getChatWebAppSettingsContract }, + textToAudio: { textToAudioChat: textToAudioChatContract }, + info: { getChatApp: getChatAppInfoContract }, + parameters: { getChatApp: getChatAppParametersContract }, + meta: { getChatApp: getChatAppMetaContract }, + site: { getChatWebAppSettings: getChatWebAppSettingsContract }, apps: { getAnnotationList: getAnnotationListContract, createAnnotation: createAnnotationContract, diff --git a/web/plugins/hey-api-orpc/config.ts b/web/plugins/hey-api-orpc/config.ts index d86f2b44e4..90bfacff26 100644 --- a/web/plugins/hey-api-orpc/config.ts +++ b/web/plugins/hey-api-orpc/config.ts @@ -4,11 +4,72 @@ import { definePluginConfig } from '@hey-api/openapi-ts' import { handler } from './plugin' +function capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +// Convert kebab-case to camelCase: "chat-messages" → "chatMessages" +function toCamelCase(str: string): string { + return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) +} + +// Default: extract first path segment and convert to camelCase +// "/chat-messages/{id}" → "chatMessages" +function defaultGroupKeyBuilder(path: string): string { + const segment = path.split('/').filter(Boolean)[0] || 'common' + return toCamelCase(segment) +} + +// Build patterns from segment name (camelCase group key) +// "chatMessages" → ["ChatMessages", "ChatMessage"] +function buildGroupPatterns(groupKey: string): string[] { + const patterns: string[] = [] + const pascalKey = capitalizeFirst(groupKey) + patterns.push(pascalKey) + + // Singular form: "ChatMessages" → "ChatMessage" + if (pascalKey.endsWith('s') && !pascalKey.endsWith('ss')) { + patterns.push(pascalKey.slice(0, -1)) + } + + return patterns +} + +// Default: simplify operationId by removing redundant group-based patterns +// e.g., "sendChatMessage" with groupKey "chatMessages" → "send" +// e.g., "getConversationsList" with groupKey "conversations" → "getList" +function defaultOperationKeyBuilder(operationId: string, groupKey: string): string { + const patternsToRemove = buildGroupPatterns(groupKey) + + let simplified = operationId + + // Remove patterns iteratively + for (const pattern of patternsToRemove) { + const regex = new RegExp(pattern, 'g') + const result = simplified.replace(regex, '') + if (result !== simplified && result.length > 0) { + simplified = result + } + } + + // Ensure first char is lowercase + simplified = simplified.charAt(0).toLowerCase() + simplified.slice(1) + + // Handle edge cases where we end up with just HTTP method or too short + if (!simplified || simplified.length < 2) { + return operationId.charAt(0).toLowerCase() + operationId.slice(1) + } + + return simplified +} + export const defaultConfig: OrpcPlugin['Config'] = { config: { contractNameBuilder: (id: string) => `${id}Contract`, defaultTag: 'default', exportFromIndex: false, + groupKeyBuilder: defaultGroupKeyBuilder, + operationKeyBuilder: defaultOperationKeyBuilder, output: 'orpc', }, dependencies: ['@hey-api/typescript', 'zod'], @@ -19,6 +80,8 @@ export const defaultConfig: OrpcPlugin['Config'] = { plugin.config.exportFromIndex ??= false plugin.config.contractNameBuilder ??= (id: string) => `${id}Contract` plugin.config.defaultTag ??= 'default' + plugin.config.groupKeyBuilder ??= defaultGroupKeyBuilder + plugin.config.operationKeyBuilder ??= defaultOperationKeyBuilder }, tags: ['client'], } diff --git a/web/plugins/hey-api-orpc/plugin.ts b/web/plugins/hey-api-orpc/plugin.ts index e285e3f658..376bcc6b61 100644 --- a/web/plugins/hey-api-orpc/plugin.ts +++ b/web/plugins/hey-api-orpc/plugin.ts @@ -3,85 +3,6 @@ import type { OrpcPlugin } from './types' import { $ } from '@hey-api/openapi-ts' -function capitalizeFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1) -} - -// Convert kebab-case to camelCase: "chat-messages" → "chatMessages" -function toCamelCase(str: string): string { - return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) -} - -// Extract first path segment: "/chat-messages" → "chat-messages" -function getPathSegment(path: string): string { - return path.split('/').filter(Boolean)[0] || 'common' -} - -// Simplify operation key by removing redundant parts based on the group -// e.g., "sendChatMessage" with segment "chat-messages" → "send" -// e.g., "getConversationsList" with segment "conversations" → "list" -// e.g., "uploadChatFile" with segment "files" → "upload" -function simplifyOperationKey(operationId: string, segment: string): string { - // Patterns to remove (order matters - more specific first) - const patternsToRemove = [ - // App-specific patterns - 'ChatWebApp', - 'ChatApp', - 'ChatFile', - 'ChatMessage', - 'Chat', - // Segment-based patterns - ...buildSegmentPatterns(segment), - ] - - let simplified = operationId - - // Remove patterns iteratively - for (const pattern of patternsToRemove) { - const regex = new RegExp(pattern, 'g') - const result = simplified.replace(regex, '') - if (result !== simplified && result.length > 0) { - simplified = result - } - } - - // Ensure first char is lowercase - simplified = simplified.charAt(0).toLowerCase() + simplified.slice(1) - - // Handle edge cases where we end up with just HTTP method - // e.g., "get" → keep as "get", but "getChatApp" → "get" is fine for single operations - if (!simplified || simplified.length < 2) { - return operationId.charAt(0).toLowerCase() + operationId.slice(1) - } - - return simplified -} - -// Build patterns from segment name -// "chat-messages" → ["ChatMessages", "ChatMessage"] -// "conversations" → ["Conversations", "Conversation"] -// "audio-to-text" → ["AudioToText"] -function buildSegmentPatterns(segment: string): string[] { - const parts = segment.split('-') - const patterns: string[] = [] - - // Full camelCase: "chat-messages" → "ChatMessages" - const fullCamel = parts.map(capitalizeFirst).join('') - patterns.push(fullCamel) - - // Singular form: "ChatMessages" → "ChatMessage" - if (fullCamel.endsWith('s') && !fullCamel.endsWith('ss')) { - patterns.push(fullCamel.slice(0, -1)) - } - - return patterns -} - -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 operationId?: string @@ -94,8 +15,6 @@ type OperationInfo = { hasInput: boolean hasOutput: boolean successStatusCode?: number - zodDataSchema: string - zodResponseSchema: string } function collectOperation(operation: IR.OperationObject, defaultTag: string): OperationInfo { @@ -134,8 +53,6 @@ function collectOperation(operation: IR.OperationObject, defaultTag: string): Op successStatusCode, summary: operation.summary, tags: operation.tags && operation.tags.length > 0 ? [...operation.tags] : [defaultTag], - zodDataSchema: toZodSchemaName(id, 'data'), - zodResponseSchema: toZodSchemaName(id, 'response'), } } @@ -143,6 +60,8 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => { const { contractNameBuilder, defaultTag, + groupKeyBuilder, + operationKeyBuilder, } = plugin.config const operations: OperationInfo[] = [] @@ -304,26 +223,25 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => { }, }) - // Group operations by path segment - const operationsBySegment = new Map() + // Group operations by group key + const operationsByGroup = new Map() for (const op of operations) { - const segment = getPathSegment(op.path) - if (!operationsBySegment.has(segment)) { - operationsBySegment.set(segment, []) + const groupKey = groupKeyBuilder(op.path) + if (!operationsByGroup.has(groupKey)) { + operationsByGroup.set(groupKey, []) } - operationsBySegment.get(segment)!.push(op) + operationsByGroup.get(groupKey)!.push(op) } // Build nested contracts object const contractsObject = $.object() - for (const [segment, segmentOps] of operationsBySegment) { - const groupKey = toCamelCase(segment) + for (const [groupKey, groupOps] of operationsByGroup) { const groupObject = $.object() - for (const op of segmentOps) { + for (const op of groupOps) { const contractSymbol = contractSymbols[op.id] if (contractSymbol) { - const key = simplifyOperationKey(op.id, segment) + const key = operationKeyBuilder(op.id, groupKey) groupObject.prop(key, $(contractSymbol)) } } diff --git a/web/plugins/hey-api-orpc/types.d.ts b/web/plugins/hey-api-orpc/types.d.ts index d65295ce9e..65e479a546 100644 --- a/web/plugins/hey-api-orpc/types.d.ts +++ b/web/plugins/hey-api-orpc/types.d.ts @@ -22,6 +22,16 @@ export type UserConfig = Plugin.Name<'orpc'> * @default 'default' */ defaultTag?: string + /** + * Custom function to extract group key from path for router grouping. + * @default (path) => path.split('/').filter(Boolean)[0] || 'common' + */ + groupKeyBuilder?: (path: string) => string + /** + * Custom function to generate operation key within a group. + * @default (operationId, groupKey) => simplified operationId + */ + operationKeyBuilder?: (operationId: string, groupKey: string) => string } export type Config = Plugin.Name<'orpc'> @@ -30,6 +40,8 @@ export type Config = Plugin.Name<'orpc'> exportFromIndex: boolean contractNameBuilder: (operationId: string) => string defaultTag: string + groupKeyBuilder: (path: string) => string + operationKeyBuilder: (operationId: string, groupKey: string) => string } export type OrpcPlugin = DefinePlugin