mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
fileStrategy
This commit is contained in:
@ -17,6 +17,10 @@ export const defaultConfig: OrpcPlugin['Config'] = {
|
||||
plugin.config.groupBy = plugin.config.groupBy ?? 'tag'
|
||||
plugin.config.contractNameBuilder = plugin.config.contractNameBuilder
|
||||
?? ((id: string) => `${id}Contract`)
|
||||
plugin.config.fileStrategy = plugin.config.fileStrategy ?? 'single'
|
||||
plugin.config.filePathBuilder = plugin.config.filePathBuilder
|
||||
?? ((tag: string) => `orpc/${tag.toLowerCase()}`)
|
||||
plugin.config.defaultTag = plugin.config.defaultTag ?? 'default'
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ type OperationInfo = {
|
||||
zodResponseSchema: string
|
||||
}
|
||||
|
||||
function collectOperation(operation: IR.OperationObject): OperationInfo {
|
||||
function collectOperation(operation: IR.OperationObject, defaultTag: string): OperationInfo {
|
||||
const id = operation.id || `${operation.method}_${operation.path.replace(/[{}/]/g, '_')}`
|
||||
|
||||
const hasPathParams = Boolean(operation.parameters?.path && Object.keys(operation.parameters.path).length > 0)
|
||||
@ -63,31 +63,46 @@ function collectOperation(operation: IR.OperationObject): OperationInfo {
|
||||
path: operation.path,
|
||||
successStatusCode,
|
||||
summary: operation.summary,
|
||||
tags: operation.tags ? [...operation.tags] : [],
|
||||
tags: operation.tags && operation.tags.length > 0 ? [...operation.tags] : [defaultTag],
|
||||
zodDataSchema: toZodSchemaName(id, 'data'),
|
||||
zodResponseSchema: toZodSchemaName(id, 'response'),
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: OrpcPlugin['Handler'] = ({ plugin }) => {
|
||||
const { contractNameBuilder, groupBy } = plugin.config
|
||||
const {
|
||||
contractNameBuilder,
|
||||
defaultTag,
|
||||
filePathBuilder,
|
||||
fileStrategy,
|
||||
groupBy,
|
||||
output,
|
||||
} = plugin.config
|
||||
|
||||
const operations: OperationInfo[] = []
|
||||
const zodImports = new Set<string>()
|
||||
|
||||
// Collect all operations using hey-api's forEach
|
||||
plugin.forEach('operation', (event) => {
|
||||
const info = collectOperation(event.operation)
|
||||
const info = collectOperation(event.operation, defaultTag)
|
||||
operations.push(info)
|
||||
|
||||
// Collect zod imports
|
||||
if (info.hasInput) {
|
||||
zodImports.add(info.zodDataSchema)
|
||||
}
|
||||
if (info.hasOutput) {
|
||||
zodImports.add(info.zodResponseSchema)
|
||||
}
|
||||
})
|
||||
|
||||
// Helper to get file path for a tag
|
||||
const getFilePathForTag = (tag: string) => {
|
||||
if (fileStrategy === 'single') {
|
||||
return output
|
||||
}
|
||||
return filePathBuilder(tag)
|
||||
}
|
||||
|
||||
// Get all unique tags
|
||||
const allTags = new Set<string>()
|
||||
for (const op of operations) {
|
||||
for (const tag of op.tags) {
|
||||
allTags.add(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Register external symbols for imports
|
||||
const symbolOc = plugin.symbol('oc', {
|
||||
exported: false,
|
||||
@ -95,35 +110,59 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => {
|
||||
})
|
||||
const symbolZ = plugin.external('zod.z')
|
||||
|
||||
// Register zod schema symbols (they come from zod plugin)
|
||||
const zodSchemaSymbols: Record<string, ReturnType<typeof plugin.symbol>> = {}
|
||||
for (const schemaName of zodImports) {
|
||||
zodSchemaSymbols[schemaName] = plugin.symbol(schemaName, {
|
||||
exported: false,
|
||||
external: './zod.gen',
|
||||
})
|
||||
// Create base contract symbol - one per file when using byTags
|
||||
const baseSymbols: Record<string, ReturnType<typeof plugin.symbol>> = {}
|
||||
|
||||
if (fileStrategy === 'byTags') {
|
||||
// Create base symbol for each tag file
|
||||
for (const tag of allTags) {
|
||||
const filePath = getFilePathForTag(tag)
|
||||
baseSymbols[tag] = plugin.symbol('base', {
|
||||
exported: true,
|
||||
getFilePath: () => filePath,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
const baseNode = $.const(baseSymbols[tag])
|
||||
.export()
|
||||
.assign(
|
||||
$(symbolOc)
|
||||
.attr('$route')
|
||||
.call(
|
||||
$.object()
|
||||
.prop('inputStructure', $.literal('detailed'))
|
||||
.prop('outputStructure', $.literal('detailed')),
|
||||
),
|
||||
)
|
||||
plugin.node(baseNode)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Single base symbol for all operations
|
||||
const baseSymbol = plugin.symbol('base', {
|
||||
exported: true,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
},
|
||||
})
|
||||
baseSymbols.__default__ = baseSymbol
|
||||
|
||||
// Create base contract: export const base = oc.$route({ inputStructure: 'detailed', outputStructure: 'detailed' })
|
||||
const baseSymbol = plugin.symbol('base', {
|
||||
exported: true,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
},
|
||||
})
|
||||
|
||||
const baseNode = $.const(baseSymbol)
|
||||
.export()
|
||||
.assign(
|
||||
$(symbolOc)
|
||||
.attr('$route')
|
||||
.call(
|
||||
$.object()
|
||||
.prop('inputStructure', $.literal('detailed'))
|
||||
.prop('outputStructure', $.literal('detailed')),
|
||||
),
|
||||
)
|
||||
plugin.node(baseNode)
|
||||
const baseNode = $.const(baseSymbol)
|
||||
.export()
|
||||
.assign(
|
||||
$(symbolOc)
|
||||
.attr('$route')
|
||||
.call(
|
||||
$.object()
|
||||
.prop('inputStructure', $.literal('detailed'))
|
||||
.prop('outputStructure', $.literal('detailed')),
|
||||
),
|
||||
)
|
||||
plugin.node(baseNode)
|
||||
}
|
||||
|
||||
// Create contract for each operation
|
||||
// Store symbols for later use in contracts object
|
||||
@ -131,16 +170,26 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => {
|
||||
|
||||
for (const op of operations) {
|
||||
const contractName = contractNameBuilder(op.id)
|
||||
const primaryTag = op.tags[0]
|
||||
const filePath = getFilePathForTag(primaryTag)
|
||||
|
||||
const contractSymbol = plugin.symbol(contractName, {
|
||||
exported: true,
|
||||
getFilePath: fileStrategy === 'byTags' ? () => filePath : undefined,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
resource: 'operation',
|
||||
resourceId: op.id,
|
||||
tag: primaryTag,
|
||||
},
|
||||
})
|
||||
contractSymbols[op.id] = contractSymbol
|
||||
|
||||
// Get the appropriate base symbol
|
||||
const baseSymbol = fileStrategy === 'byTags'
|
||||
? baseSymbols[primaryTag]
|
||||
: baseSymbols.__default__
|
||||
|
||||
// Build the route config object with all available properties
|
||||
const routeConfig = $.object()
|
||||
.prop('path', $.literal(op.path))
|
||||
@ -173,15 +222,31 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => {
|
||||
|
||||
// .input(zodDataSchema) if has input
|
||||
if (op.hasInput) {
|
||||
// Reference zod schema symbol dynamically from zod plugin
|
||||
const zodDataSymbol = plugin.referenceSymbol({
|
||||
category: 'schema',
|
||||
resource: 'operation',
|
||||
resourceId: op.id,
|
||||
role: 'data',
|
||||
tool: 'zod',
|
||||
})
|
||||
expression = expression
|
||||
.attr('input')
|
||||
.call($(zodSchemaSymbols[op.zodDataSchema]))
|
||||
.call($(zodDataSymbol))
|
||||
}
|
||||
|
||||
// .output(z.object({ status: z.literal(200), body: zodResponseSchema })) if has output (detailed outputStructure)
|
||||
if (op.hasOutput) {
|
||||
// Reference zod response schema symbol dynamically from zod plugin
|
||||
const zodResponseSymbol = plugin.referenceSymbol({
|
||||
category: 'schema',
|
||||
resource: 'operation',
|
||||
resourceId: op.id,
|
||||
role: 'responses',
|
||||
tool: 'zod',
|
||||
})
|
||||
const outputObject = $.object()
|
||||
.prop('body', $(zodSchemaSymbols[op.zodResponseSchema]))
|
||||
.prop('body', $(zodResponseSymbol))
|
||||
|
||||
// Add status code if available
|
||||
if (op.successStatusCode) {
|
||||
@ -220,72 +285,120 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => {
|
||||
plugin.node(contractNode)
|
||||
}
|
||||
|
||||
// Create contracts object export
|
||||
const contractsSymbol = plugin.symbol('contracts', {
|
||||
exported: true,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
},
|
||||
})
|
||||
// Create contracts object export (only for single file strategy)
|
||||
// For byTags, each file has its own exports
|
||||
if (fileStrategy === 'single') {
|
||||
const contractsSymbol = plugin.symbol('contracts', {
|
||||
exported: true,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
},
|
||||
})
|
||||
|
||||
if (groupBy === 'tag') {
|
||||
// Group operations by tag
|
||||
const operationsByTag = new Map<string, OperationInfo[]>()
|
||||
for (const op of operations) {
|
||||
const tag = op.tags[0]
|
||||
if (!operationsByTag.has(tag)) {
|
||||
operationsByTag.set(tag, [])
|
||||
if (groupBy === 'tag') {
|
||||
// Group operations by tag
|
||||
const operationsByTag = new Map<string, OperationInfo[]>()
|
||||
for (const op of operations) {
|
||||
const tag = op.tags[0]
|
||||
if (!operationsByTag.has(tag)) {
|
||||
operationsByTag.set(tag, [])
|
||||
}
|
||||
operationsByTag.get(tag)!.push(op)
|
||||
}
|
||||
operationsByTag.get(tag)!.push(op)
|
||||
|
||||
// Build contracts object grouped by tag
|
||||
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) {
|
||||
const contractName = contractNameBuilder(op.id)
|
||||
tagObject.prop(contractName, $(contractSymbol))
|
||||
}
|
||||
}
|
||||
contractsObject.prop(tagKey, tagObject)
|
||||
}
|
||||
|
||||
const contractsNode = $.const(contractsSymbol)
|
||||
.export()
|
||||
.assign(contractsObject)
|
||||
plugin.node(contractsNode)
|
||||
}
|
||||
else {
|
||||
// Flat structure without grouping
|
||||
const contractsObject = $.object()
|
||||
for (const op of operations) {
|
||||
const contractSymbol = contractSymbols[op.id]
|
||||
if (contractSymbol) {
|
||||
const contractName = contractNameBuilder(op.id)
|
||||
contractsObject.prop(contractName, $(contractSymbol))
|
||||
}
|
||||
}
|
||||
|
||||
const contractsNode = $.const(contractsSymbol)
|
||||
.export()
|
||||
.assign(contractsObject)
|
||||
plugin.node(contractsNode)
|
||||
}
|
||||
|
||||
// Build contracts object grouped by tag
|
||||
const contractsObject = $.object()
|
||||
for (const [tag, tagOps] of operationsByTag) {
|
||||
const tagKey = tag.charAt(0).toLowerCase() + tag.slice(1)
|
||||
const tagObject = $.object()
|
||||
// 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)
|
||||
}
|
||||
else {
|
||||
// For byTags strategy, create contracts object per file
|
||||
for (const tag of allTags) {
|
||||
const filePath = getFilePathForTag(tag)
|
||||
const tagOps = operations.filter(op => op.tags[0] === tag)
|
||||
|
||||
const contractsSymbol = plugin.symbol('contracts', {
|
||||
exported: true,
|
||||
getFilePath: () => filePath,
|
||||
meta: {
|
||||
category: 'schema',
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
const contractsObject = $.object()
|
||||
for (const op of tagOps) {
|
||||
const contractSymbol = contractSymbols[op.id]
|
||||
if (contractSymbol) {
|
||||
const contractName = contractNameBuilder(op.id)
|
||||
tagObject.prop(contractName, $(contractSymbol))
|
||||
contractsObject.prop(contractName, $(contractSymbol))
|
||||
}
|
||||
}
|
||||
contractsObject.prop(tagKey, tagObject)
|
||||
|
||||
const contractsNode = $.const(contractsSymbol)
|
||||
.export()
|
||||
.assign(contractsObject)
|
||||
plugin.node(contractsNode)
|
||||
|
||||
// Create type export per file
|
||||
const contractsTypeSymbol = plugin.symbol('Contracts', {
|
||||
exported: true,
|
||||
getFilePath: () => filePath,
|
||||
meta: {
|
||||
category: 'type',
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
const contractsTypeNode = $.type.alias(contractsTypeSymbol)
|
||||
.export()
|
||||
.type($.type.query($(contractsSymbol)))
|
||||
plugin.node(contractsTypeNode)
|
||||
}
|
||||
|
||||
const contractsNode = $.const(contractsSymbol)
|
||||
.export()
|
||||
.assign(contractsObject)
|
||||
plugin.node(contractsNode)
|
||||
}
|
||||
else {
|
||||
// Flat structure without grouping
|
||||
const contractsObject = $.object()
|
||||
for (const op of operations) {
|
||||
const contractSymbol = contractSymbols[op.id]
|
||||
if (contractSymbol) {
|
||||
const contractName = contractNameBuilder(op.id)
|
||||
contractsObject.prop(contractName, $(contractSymbol))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { DefinePlugin } from '@hey-api/openapi-ts'
|
||||
|
||||
export type FileStrategy = 'single' | 'byTags'
|
||||
|
||||
export type Config = { name: 'orpc' } & {
|
||||
/**
|
||||
* Name of the generated file.
|
||||
* Name of the generated file (when fileStrategy is 'single').
|
||||
* @default 'orpc'
|
||||
*/
|
||||
output?: string
|
||||
@ -23,6 +25,23 @@ export type Config = { name: 'orpc' } & {
|
||||
* @default 'tag'
|
||||
*/
|
||||
groupBy?: 'tag' | 'none'
|
||||
/**
|
||||
* File generation strategy.
|
||||
* - 'single': All contracts in one file (default)
|
||||
* - 'byTags': One file per tag (e.g., orpc/chat.ts, orpc/files.ts)
|
||||
* @default 'single'
|
||||
*/
|
||||
fileStrategy?: FileStrategy
|
||||
/**
|
||||
* Custom file path builder when fileStrategy is 'byTags'.
|
||||
* @default (tag) => `orpc/${tag}`
|
||||
*/
|
||||
filePathBuilder?: (tag: string) => string
|
||||
/**
|
||||
* Default tag name for operations without tags.
|
||||
* @default 'default'
|
||||
*/
|
||||
defaultTag?: string
|
||||
}
|
||||
|
||||
export type ResolvedConfig = {
|
||||
@ -31,6 +50,9 @@ export type ResolvedConfig = {
|
||||
exportFromIndex: boolean
|
||||
contractNameBuilder: (operationId: string) => string
|
||||
groupBy: 'tag' | 'none'
|
||||
fileStrategy: FileStrategy
|
||||
filePathBuilder: (tag: string) => string
|
||||
defaultTag: string
|
||||
}
|
||||
|
||||
export type OrpcPlugin = DefinePlugin<Config, ResolvedConfig>
|
||||
|
||||
Reference in New Issue
Block a user