diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/index.ts b/src/codegen/generators/typescript/channels/protocols/amqp/index.ts index 8ba135d..9495c3a 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/index.ts @@ -96,7 +96,7 @@ async function generateForOperations( ): Promise { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; const exchangeName = channel.bindings().get('amqp')?.value()?.exchange?.name; for (const operation of channel.operations().all()) { @@ -192,7 +192,7 @@ async function generateForChannels( const {generator, payloads} = context; const functionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? - generator.functionTypeMapping[channel.id()]; + generator.functionTypeMapping?.[channel.id()]; const exchangeName = channel.bindings().get('amqp')?.value()?.exchange?.name; const payload = payloads.channelModels[channel.id()]; diff --git a/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts b/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts index f04e4f4..ff36d06 100644 --- a/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts @@ -97,7 +97,7 @@ async function generateForOperations( ): Promise { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = @@ -188,7 +188,7 @@ async function generateForChannels( const {generator, payloads} = context; const functionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? - generator.functionTypeMapping[channel.id()]; + generator.functionTypeMapping?.[channel.id()]; const payload = payloads.channelModels[channel.id()]; if (!payload) { diff --git a/src/codegen/generators/typescript/channels/protocols/http/index.ts b/src/codegen/generators/typescript/channels/protocols/http/index.ts index 149aadc..a92add3 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/index.ts @@ -110,7 +110,7 @@ function generateForOperations( ): HttpRenderType[] { const renders: HttpRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = diff --git a/src/codegen/generators/typescript/channels/protocols/kafka/index.ts b/src/codegen/generators/typescript/channels/protocols/kafka/index.ts index 37cef53..9565f26 100644 --- a/src/codegen/generators/typescript/channels/protocols/kafka/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/kafka/index.ts @@ -97,7 +97,7 @@ async function generateForOperations( ): Promise { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = @@ -182,7 +182,7 @@ async function generateForChannels( const {generator, payloads} = context; const functionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? - generator.functionTypeMapping[channel.id()]; + generator.functionTypeMapping?.[channel.id()]; const payload = payloads.channelModels[channel.id()]; if (!payload) { diff --git a/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts b/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts index 7657362..32ff2cb 100644 --- a/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts @@ -95,7 +95,7 @@ function generateForOperations( ): SingleFunctionRenderType[] { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = @@ -157,7 +157,7 @@ function generateForChannels( ): SingleFunctionRenderType[] { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; const updatedFunctionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? functionTypeMapping; diff --git a/src/codegen/generators/typescript/channels/protocols/nats/index.ts b/src/codegen/generators/typescript/channels/protocols/nats/index.ts index c99a1af..b2bb069 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/index.ts @@ -112,7 +112,7 @@ async function generateForOperations( ): Promise { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = @@ -325,7 +325,7 @@ async function generateForChannels( const {generator, payloads} = context; const functionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? - generator.functionTypeMapping[channel.id()]; + generator.functionTypeMapping?.[channel.id()]; const payload = payloads.channelModels[channel.id()]; if (!payload) { diff --git a/src/codegen/generators/typescript/channels/protocols/websocket/index.ts b/src/codegen/generators/typescript/channels/protocols/websocket/index.ts index 952e0bd..27febfe 100644 --- a/src/codegen/generators/typescript/channels/protocols/websocket/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/websocket/index.ts @@ -106,7 +106,7 @@ async function generateForOperations( ): Promise { const renders: SingleFunctionRenderType[] = []; const {generator, payloads} = context; - const functionTypeMapping = generator.functionTypeMapping[channel.id()]; + const functionTypeMapping = generator.functionTypeMapping?.[channel.id()]; for (const operation of channel.operations().all()) { const updatedFunctionTypeMapping = @@ -208,7 +208,7 @@ async function generateForChannels( const {generator, payloads} = context; const functionTypeMapping = getFunctionTypeMappingFromAsyncAPI(channel) ?? - generator.functionTypeMapping[channel.id()]; + generator.functionTypeMapping?.[channel.id()]; const payload = payloads.channelModels[channel.id()]; if (!payload) { diff --git a/test/codegen/generators/typescript/channels/protocols/functionTypeMapping.spec.ts b/test/codegen/generators/typescript/channels/protocols/functionTypeMapping.spec.ts new file mode 100644 index 0000000..53f8f89 --- /dev/null +++ b/test/codegen/generators/typescript/channels/protocols/functionTypeMapping.spec.ts @@ -0,0 +1,274 @@ +/** + * Tests that all protocol generators handle undefined functionTypeMapping gracefully. + * + * Bug: When generator.functionTypeMapping is undefined (instead of {}), accessing + * generator.functionTypeMapping[channel.id()] throws: + * "Cannot read properties of undefined (reading 'channel_id')" + * + * This test verifies that all 7 protocol generators have defensive access via optional chaining. + */ +import path from 'node:path'; +import { + defaultTypeScriptChannelsGenerator, + generateTypeScriptChannels, + TypeScriptParameterRenderType +} from '../../../../../../src/codegen/generators'; +import {loadAsyncapiDocument} from '../../../../../../src/codegen/inputs/asyncapi'; +import { + ConstrainedAnyModel, + ConstrainedObjectModel, + OutputModel +} from '@asyncapi/modelina'; +import {TypeScriptPayloadRenderType} from '../../../../../../src/codegen/generators/typescript/payloads'; +import {TypeScriptHeadersRenderType} from '../../../../../../src/codegen/generators/typescript/headers'; + +jest.mock('node:fs/promises', () => ({ + writeFile: jest.fn().mockResolvedValue(undefined), + mkdir: jest.fn().mockResolvedValue(undefined) +})); + +describe('functionTypeMapping undefined bug', () => { + const payloadModel = new OutputModel( + '', + new ConstrainedAnyModel('TestPayloadModel', undefined, {}, 'Payload'), + 'TestPayloadModel', + {models: {}, originalInput: undefined}, + [] + ); + + const createHeadersDependency = (): TypeScriptHeadersRenderType => ({ + channelModels: {}, + generator: {outputPath: './test'} as any + }); + + // Helper to create a generator with functionTypeMapping explicitly set to undefined + // This simulates the bug condition where spread operator preserves undefined + const createGeneratorWithUndefinedFunctionTypeMapping = (protocols: string[]) => { + const generator = { + ...defaultTypeScriptChannelsGenerator, + outputPath: path.resolve(__dirname, './output'), + id: 'test', + asyncapiGenerateForOperations: false, + protocols + }; + // Explicitly set to undefined to trigger the bug + (generator as any).functionTypeMapping = undefined; + return generator; + }; + + let parsedAsyncAPIDocument: any; + let parametersDependency: TypeScriptParameterRenderType; + let payloadsDependency: TypeScriptPayloadRenderType; + + beforeAll(async () => { + parsedAsyncAPIDocument = await loadAsyncapiDocument( + path.resolve(__dirname, '../../../../../configs/asyncapi.yaml') + ); + + const parameterModel = new OutputModel( + '', + new ConstrainedObjectModel( + 'TestParameter', + undefined, + {}, + 'Parameter', + {} + ), + 'TestParameter', + {models: {}, originalInput: undefined}, + [] + ); + + parametersDependency = { + channelModels: { + 'user/signedup': parameterModel + }, + generator: {outputPath: './test'} as any + }; + + payloadsDependency = { + channelModels: { + 'user/signedup': { + messageModel: payloadModel, + messageType: 'MessageType' + } + }, + operationModels: {}, + otherModels: [], + generator: {outputPath: './test'} as any + }; + }); + + describe('should handle undefined functionTypeMapping without crashing', () => { + it('NATS generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping(['nats']), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('Kafka generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping(['kafka']), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('MQTT generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping(['mqtt']), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('AMQP generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping(['amqp']), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('WebSocket generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping([ + 'websocket' + ]), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('EventSource generator', async () => { + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping([ + 'event_source' + ]), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('HTTP client generator', async () => { + // HTTP client needs request/reply pattern, use asyncapi-request.yaml + const requestDoc = await loadAsyncapiDocument( + path.resolve(__dirname, '../../../../../configs/asyncapi-request.yaml') + ); + + const httpPayloadsDependency: TypeScriptPayloadRenderType = { + channelModels: { + ping: { + messageModel: payloadModel, + messageType: 'MessageType' + } + }, + operationModels: { + pingRequest: { + messageModel: payloadModel, + messageType: 'MessageType' + }, + pongResponse: { + messageModel: payloadModel, + messageType: 'MessageType' + }, + pingRequest_reply: { + messageModel: payloadModel, + messageType: 'MessageType' + } + }, + otherModels: [], + generator: {outputPath: './test'} as any + }; + + const httpGenerator = createGeneratorWithUndefinedFunctionTypeMapping([ + 'http_client' + ]); + // HTTP requires asyncapiGenerateForOperations: true + httpGenerator.asyncapiGenerateForOperations = true; + + await expect( + generateTypeScriptChannels({ + generator: httpGenerator, + inputType: 'asyncapi', + asyncapiDocument: requestDoc, + dependencyOutputs: { + 'parameters-typescript': { + channelModels: {}, + generator: {outputPath: './test'} as any + }, + 'payloads-typescript': httpPayloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + + it('all protocols together', async () => { + // Test all AsyncAPI-compatible protocols together (excluding http_client which needs special setup) + await expect( + generateTypeScriptChannels({ + generator: createGeneratorWithUndefinedFunctionTypeMapping([ + 'nats', + 'kafka', + 'mqtt', + 'amqp', + 'websocket', + 'event_source' + ]), + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }) + ).resolves.not.toThrow(); + }); + }); +});