diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 1299da64e..1844458ec 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -27,15 +27,18 @@ const isNodeReturnStatement = ({ const schemaResponseTransformerNodes = ({ plugin, + processingSchemas, schema, }: { plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { const identifierData = tsc.identifier({ text: dataVariableName }); const nodes = processSchemaType({ dataExpression: identifierData, plugin, + processingSchemas, schema, }); // append return statement if one does not already exist @@ -51,19 +54,25 @@ const schemaResponseTransformerNodes = ({ const processSchemaType = ({ dataExpression, plugin, + processingSchemas, schema, }: { dataExpression?: ts.Expression | string; plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { if (schema.$ref) { const selector = plugin.api.selector('response-ref', schema.$ref); + const selectorKey = JSON.stringify(selector); if (!plugin.getSymbol(selector)) { // TODO: remove // create each schema response transformer only once + // Mark as processing to handle recursion + processingSchemas.add(selectorKey); + // Register symbol early to prevent infinite recursion with self-referential schemas const symbol = plugin.registerSymbol({ name: buildName({ @@ -81,8 +90,13 @@ const processSchemaType = ({ ); const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: refSchema, }); + + // Done processing + processingSchemas.delete(selectorKey); + if (nodes.length) { const node = tsc.constVariable({ expression: tsc.arrowFunction({ @@ -103,7 +117,9 @@ const processSchemaType = ({ } } - if (plugin.isSymbolRegistered(selector)) { + // Only reference the symbol if it has a value (transformer function was generated) + // OR is currently being processed (recursive case) + if (plugin.hasSymbolValue(selector) || processingSchemas.has(selectorKey)) { const ref = plugin.referenceSymbol(selector); const callExpression = tsc.callExpression({ functionName: ref.placeholder, @@ -145,6 +161,7 @@ const processSchemaType = ({ : processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items?.[0] ? schema.items[0] : { @@ -209,6 +226,7 @@ const processSchemaType = ({ const propertyNodes = processSchemaType({ dataExpression: propertyAccessExpression, plugin, + processingSchemas, schema: property, }); if (!propertyNodes.length) { @@ -245,6 +263,7 @@ const processSchemaType = ({ return processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items[0]!, }); } @@ -262,6 +281,7 @@ const processSchemaType = ({ const nodes = processSchemaType({ dataExpression: dataExpression || 'item', plugin, + processingSchemas, schema: item, }); if (nodes.length) { @@ -319,6 +339,9 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { + // Track schemas currently being processed to handle recursion + const processingSchemas = new Set(); + plugin.forEach( 'operation', ({ operation }) => { @@ -343,6 +366,7 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { // TODO: parser - consider handling simple string response which is also a date const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: response, }); if (!nodes.length) return; diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index f3fffd15a..08cbd8cd7 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -297,6 +297,11 @@ export class PluginInstance { return this.gen.symbols.isRegistered(symbolIdOrSelector); } + hasSymbolValue(symbolIdOrSelector: number | Selector): boolean { + const symbol = this.getSymbol(symbolIdOrSelector); + return symbol ? this.gen.symbols.hasValue(symbol.id) : false; + } + referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); }