From 8e665c6516d45dff794bb8fb08b0adef5c26631e Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 6 Sep 2025 19:34:00 +0200 Subject: [PATCH 1/9] fix headers --- .../typescript/channels/asyncapi.ts | 8 + .../generators/typescript/channels/index.ts | 23 ++- .../channels/protocols/amqp/index.ts | 1 + .../channels/protocols/eventsource/index.ts | 1 + .../channels/protocols/kafka/index.ts | 1 + .../channels/protocols/mqtt/index.ts | 1 + .../channels/protocols/nats/corePublish.ts | 10 +- .../channels/protocols/nats/coreSubscribe.ts | 34 +++- .../channels/protocols/nats/index.ts | 3 +- .../protocols/nats/jetstreamPublish.ts | 10 +- .../protocols/nats/jetstreamPullSubscribe.ts | 63 +++++++- .../nats/jetstreamPushSubscription.ts | 62 +++++++- .../generators/typescript/channels/types.ts | 12 +- .../generators/typescript/channels/utils.ts | 23 +++ test/runtime/asyncapi-regular.json | 11 +- test/runtime/typescript/src/channels/index.ts | 149 ++++++++++++++++-- .../src/headers/UserSignedUpHeaders.ts | 42 ++--- .../typescript/src/payloads/UserSignedUp.ts | 1 + .../test/channels/regular/nats.spec.ts | 116 +++++++++++++- 19 files changed, 495 insertions(+), 76 deletions(-) diff --git a/src/codegen/generators/typescript/channels/asyncapi.ts b/src/codegen/generators/typescript/channels/asyncapi.ts index de1175e5..e2b9be3c 100644 --- a/src/codegen/generators/typescript/channels/asyncapi.ts +++ b/src/codegen/generators/typescript/channels/asyncapi.ts @@ -5,6 +5,7 @@ import { } from '@asyncapi/parser'; import {TypeScriptParameterRenderType} from '../parameters'; import {TypeScriptPayloadRenderType} from '../payloads'; +import {TypeScriptHeadersRenderType} from '../headers'; import { ChannelFunctionTypes, TypeScriptChannelRenderedFunctionType, @@ -91,6 +92,7 @@ export async function generateTypeScriptChannelsForAsyncAPI( context: TypeScriptChannelsContext, parameters: TypeScriptParameterRenderType, payloads: TypeScriptPayloadRenderType, + headers: TypeScriptHeadersRenderType | undefined, protocolsToUse: SupportedProtocols[], protocolCodeFunctions: Record, externalProtocolFunctionInformation: Record< @@ -117,12 +119,18 @@ export async function generateTypeScriptChannelsForAsyncAPI( } } + let headerModel: OutputModel | undefined = undefined; + if (headers) { + headerModel = headers.channelModels[channel.id()]; + } + for (const protocol of protocolsToUse) { const protocolContext: TypeScriptChannelsGeneratorContext = { ...context, subName, topic: channel.address()!, parameter: parameter?.model as ConstrainedObjectModel, + headers: headerModel?.model as ConstrainedObjectModel, payloads }; diff --git a/src/codegen/generators/typescript/channels/index.ts b/src/codegen/generators/typescript/channels/index.ts index 64324350..15da459f 100644 --- a/src/codegen/generators/typescript/channels/index.ts +++ b/src/codegen/generators/typescript/channels/index.ts @@ -14,6 +14,7 @@ import { TypeScriptPayloadRenderType, defaultTypeScriptPayloadGenerator } from '../payloads'; +import {TypeScriptHeadersRenderType} from '../headers'; import { TypeScriptChannelRenderedFunctionType, TypeScriptChannelRenderType, @@ -24,7 +25,7 @@ import { zodTypescriptChannelsGenerator, ChannelFunctionTypes } from './types'; -import {addParametersToDependencies, addPayloadsToDependencies} from './utils'; +import {addParametersToDependencies, addPayloadsToDependencies, addHeadersToDependencies} from './utils'; import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi'; export { TypeScriptChannelRenderedFunctionType, @@ -50,7 +51,7 @@ export async function generateTypeScriptChannels( TypeScriptChannelRenderedFunctionType[] > = {}; - const {parameters, payloads} = validateContext(context); + const {parameters, payloads, headers} = validateContext(context); const generator = context.generator; const protocolsToUse = generator.protocols; @@ -59,12 +60,13 @@ export async function generateTypeScriptChannels( externalProtocolFunctionInformation[protocol] = []; } const dependencies: string[] = []; - addDependencies(payloads, parameters, context, dependencies); + addDependencies(payloads, parameters, headers, context, dependencies); if (context.inputType === 'asyncapi') { await generateTypeScriptChannelsForAsyncAPI( context, parameters, payloads, + headers, protocolsToUse, protocolCodeFunctions, externalProtocolFunctionInformation, @@ -121,6 +123,7 @@ ${Object.entries(protocolCodeFunctions) function addDependencies( payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, + headers: TypeScriptHeadersRenderType | undefined, context: TypeScriptChannelsContext, dependencies: string[] ) { @@ -148,11 +151,20 @@ function addDependencies( context.generator, dependencies ); + if (headers) { + addHeadersToDependencies( + headers.channelModels, + headers.generator, + context.generator, + dependencies + ); + } } function validateContext(context: TypeScriptChannelsContext): { payloads: TypeScriptPayloadRenderType; parameters: TypeScriptParameterRenderType; + headers: TypeScriptHeadersRenderType | undefined; } { const {generator} = context; if (!context.dependencyOutputs) { @@ -166,6 +178,9 @@ function validateContext(context: TypeScriptChannelsContext): { const parameters = context.dependencyOutputs[ generator.parameterGeneratorId ] as TypeScriptParameterRenderType; + const headers = context.dependencyOutputs[ + generator.headerGeneratorId + ] as TypeScriptHeadersRenderType | undefined; if (!payloads) { throw new Error( 'Internal error, could not determine previous rendered payloads generator that is required for channel TypeScript generator' @@ -176,7 +191,7 @@ function validateContext(context: TypeScriptChannelsContext): { 'Internal error, could not determine previous rendered parameters generator that is required for channel TypeScript generator' ); } - return {payloads, parameters}; + return {payloads, parameters, headers}; } /** diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/index.ts b/src/codegen/generators/typescript/channels/protocols/amqp/index.ts index bd2c9120..7e4f6acd 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/index.ts @@ -36,6 +36,7 @@ export async function generateAmqpChannels( const amqpContext: RenderRegularParameters = { channelParameters: parameter, + channelHeaders: undefined, // AMQP doesn't support headers in this implementation topic, messageType: '', subName: context.subName, diff --git a/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts b/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts index d0cb1f08..9cebaf0a 100644 --- a/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/eventsource/index.ts @@ -35,6 +35,7 @@ export async function generateEventSourceChannels( const eventSourceContext: RenderRegularParameters = { channelParameters: parameter, + channelHeaders: undefined, // EventSource doesn't support headers in this implementation topic, messageType: '', subName: context.subName, diff --git a/src/codegen/generators/typescript/channels/protocols/kafka/index.ts b/src/codegen/generators/typescript/channels/protocols/kafka/index.ts index a958656f..514dcd95 100644 --- a/src/codegen/generators/typescript/channels/protocols/kafka/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/kafka/index.ts @@ -37,6 +37,7 @@ export async function generateKafkaChannels( const kafkaContext: RenderRegularParameters = { channelParameters: parameter, + channelHeaders: undefined, // Kafka doesn't support headers in this implementation topic: kafkaTopic, messageType: '', subName: context.subName, diff --git a/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts b/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts index cfd6e009..e6749f6a 100644 --- a/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/mqtt/index.ts @@ -33,6 +33,7 @@ export async function generateMqttChannels( const ignoreOperation = !generator.asyncapiGenerateForOperations; const mqttContext: RenderRegularParameters = { channelParameters: parameter, + channelHeaders: undefined, // MQTT doesn't support headers in this implementation topic, subName: context.subName, messageType: '', diff --git a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts index 788a029b..7720fe4e 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts @@ -3,12 +3,14 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; +import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; export function renderCorePublish({ topic, messageType, messageModule, channelParameters, + channelHeaders, subName = pascalCase(topic), functionName = `publishTo${subName}` }: RenderRegularParameters): SingleFunctionRenderType { @@ -20,13 +22,18 @@ export function renderCorePublish({ messageMarshalling = `${messageModule}.marshal(message)`; } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; + const headerSetup = generateHeaderSetupCode(channelHeaders); + const publishOperation = messageType === 'null' - ? `await nc.publish(${addressToUse}, Nats.Empty, options);` + ? `${headerSetup} + await nc.publish(${addressToUse}, Nats.Empty, options);` : `let dataToSend: any = ${messageMarshalling}; + ${headerSetup} dataToSend = codec.encode(dataToSend); nc.publish(${addressToUse}, dataToSend, options);`; + const headerParam = generateHeaderParameter(channelHeaders); const functionParameters = [ { parameter: `message`, @@ -42,6 +49,7 @@ nc.publish(${addressToUse}, dataToSend, options);`; } ] : []), + ...(headerParam ? [headerParam] : []), { parameter: 'nc', parameterType: 'nc: Nats.NatsConnection', diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts index 7a1ff804..a038ffef 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts @@ -5,12 +5,14 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {getValidationFunctions} from '../../utils'; +import {generateHeaderExtractionCode, generateHeaderCallbackParameter} from './utils'; export function renderCoreSubscribe({ topic, messageType, messageModule, channelParameters, + channelHeaders, subName = pascalCase(topic), payloadGenerator, functionName = `subscribeTo${subName}` @@ -29,13 +31,18 @@ export function renderCoreSubscribe({ includeValidation, messageModule, messageType, - onValidationFail: channelParameters + onValidationFail: channelParameters && channelHeaders + ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, extractedHeaders, msg); continue;` + : channelParameters ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, msg); continue;` + : channelHeaders + ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, extractedHeaders, msg); continue;` : `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, msg); continue;` }); messageType = messageModule ? `${messageModule}.${messageType}` : messageType; + const headerCallbackParam = generateHeaderCallbackParameter(channelHeaders); const callbackFunctionParameters = [ { parameter: 'err?: Error', @@ -53,6 +60,7 @@ export function renderCoreSubscribe({ } ] : []), + ...(headerCallbackParam ? [headerCallbackParam] : []), { parameter: `natsMsg?: Nats.Msg`, jsDoc: ' * @param natsMsg' @@ -97,8 +105,20 @@ export function renderCoreSubscribe({ ' * @param skipMessageValidation turn off runtime validation of incoming messages' } ]; + const headerExtraction = generateHeaderExtractionCode(channelHeaders); + let whenReceivingMessage = ''; - if (channelParameters) { + if (channelParameters && channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; + } + } else if (channelParameters) { if (messageType === 'null') { whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; } else { @@ -106,6 +126,16 @@ export function renderCoreSubscribe({ ${potentialValidationFunction} onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; } + } else if (channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; + } } else if (messageType === 'null') { whenReceivingMessage = `onDataCallback(undefined, null, msg);`; } else { diff --git a/src/codegen/generators/typescript/channels/protocols/nats/index.ts b/src/codegen/generators/typescript/channels/protocols/nats/index.ts index 113aeca4..fbc4cdeb 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/index.ts @@ -48,13 +48,14 @@ export async function generateNatsChannels( >, dependencies: string[] ) { - const {parameter, topic, payloads} = context; + const {parameter, headers, topic, payloads} = context; const ignoreOperation = !context.generator.asyncapiGenerateForOperations; let natsTopic = topic.startsWith('/') ? topic.slice(1) : topic; natsTopic = natsTopic.replace(/\//g, '.'); const natsContext: RenderRegularParameters = { channelParameters: parameter, + channelHeaders: headers, topic: natsTopic, messageType: '', subName: context.subName, diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts index e4a7427a..306acc3a 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts @@ -3,11 +3,13 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {ChannelFunctionTypes} from '../../index'; import {RenderRegularParameters} from '../../types'; +import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; export function renderJetstreamPublish({ topic, messageType, messageModule, channelParameters, + channelHeaders, subName = pascalCase(topic), functionName = `jetStreamPublishTo${subName}` }: RenderRegularParameters): SingleFunctionRenderType { @@ -20,13 +22,18 @@ export function renderJetstreamPublish({ } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; + const headerSetup = generateHeaderSetupCode(channelHeaders); + const publishOperation = messageType === 'null' - ? `await js.publish(${addressToUse}, Nats.Empty, options);` + ? `${headerSetup} + await js.publish(${addressToUse}, Nats.Empty, options);` : `let dataToSend: any = ${messageMarshalling}; + ${headerSetup} dataToSend = codec.encode(dataToSend); await js.publish(${addressToUse}, dataToSend, options);`; + const headerParam = generateHeaderParameter(channelHeaders); const functionParameters = [ { parameter: `message`, @@ -42,6 +49,7 @@ await js.publish(${addressToUse}, dataToSend, options);`; } ] : []), + ...(headerParam ? [headerParam] : []), { parameter: 'js', parameterType: 'js: Nats.JetStreamClient', diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts index 6ddf1997..b7b09b15 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts @@ -5,12 +5,14 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {getValidationFunctions} from '../../utils'; +import {generateHeaderExtractionCode, generateHeaderCallbackParameter} from './utils'; export function renderJetstreamPullSubscribe({ topic, messageType, messageModule, channelParameters, + channelHeaders, subName = pascalCase(topic), payloadGenerator, functionName = `jetStreamPullSubscribeTo${subName}` @@ -52,6 +54,14 @@ export function renderJetstreamPullSubscribe({ } ] : []), + ...(channelHeaders + ? [ + { + parameter: `headers?: ${channelHeaders.type}`, + jsDoc: ' * @param headers that was received with the message' + } + ] + : []), {parameter: 'jetstreamMsg?: Nats.JsMsg', jsDoc: ' * @param jetstreamMsg'} ]; @@ -95,17 +105,54 @@ export function renderJetstreamPullSubscribe({ } ]; - const whenReceivingMessage = channelParameters - ? messageType === 'null' - ? `onDataCallback(undefined, null, parameters, msg);` - : `let receivedData: any = codec.decode(msg.data); + const headerExtraction = channelHeaders + ? `// Extract headers if present + let extractedHeaders: ${channelHeaders.type} | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); + }` + : ''; + + let whenReceivingMessage = ''; + if (channelParameters && channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} ${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);` - : messageType === 'null' - ? `onDataCallback(undefined, null, msg);` - : `let receivedData: any = codec.decode(msg.data); +onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; + } + } else if (channelParameters) { + if (messageType === 'null') { + whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; + } + } else if (channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; + } + } else if (messageType === 'null') { + whenReceivingMessage = `onDataCallback(undefined, null, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); ${potentialValidationFunction} onDataCallback(undefined, ${messageUnmarshalling}, msg);`; + } const jsDocParameters = functionParameters .map((param) => param.jsDoc) diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts index be96bb05..0469ab69 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts @@ -11,6 +11,7 @@ export function renderJetstreamPushSubscription({ messageType, messageModule, channelParameters, + channelHeaders, subName = pascalCase(topic), payloadGenerator, functionName = `jetStreamPushSubscriptionFrom${subName}` @@ -52,6 +53,14 @@ export function renderJetstreamPushSubscription({ } ] : []), + ...(channelHeaders + ? [ + { + parameter: `headers?: ${channelHeaders.type}`, + jsDoc: ' * @param headers that was received with the message' + } + ] + : []), { parameter: 'jetstreamMsg?: Nats.JsMsg', jsDoc: ' * @param jetstreamMsg' @@ -98,17 +107,54 @@ export function renderJetstreamPushSubscription({ } ]; - const whenReceivingMessage = channelParameters - ? messageType === 'null' - ? `onDataCallback(undefined, null, parameters, msg);` - : `let receivedData: any = codec.decode(msg.data); + const headerExtraction = channelHeaders + ? `// Extract headers if present + let extractedHeaders: ${channelHeaders.type} | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); + }` + : ''; + + let whenReceivingMessage = ''; + if (channelParameters && channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} ${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);` - : messageType === 'null' - ? `onDataCallback(undefined, null, msg);` - : `let receivedData: any = codec.decode(msg.data); +onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; + } + } else if (channelParameters) { + if (messageType === 'null') { + whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; + } + } else if (channelHeaders) { + if (messageType === 'null') { + whenReceivingMessage = `${headerExtraction} + onDataCallback(undefined, null, extractedHeaders, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); +${headerExtraction} +${potentialValidationFunction} +onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; + } + } else if (messageType === 'null') { + whenReceivingMessage = `onDataCallback(undefined, null, msg);`; + } else { + whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); ${potentialValidationFunction} onDataCallback(undefined, ${messageUnmarshalling}, msg);`; + } const jsDocParameters = functionParameters .map((param) => param.jsDoc) diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 9e20d6f9..d7e904a5 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -30,7 +30,7 @@ export const zodTypescriptChannelsGenerator = z.object({ dependencies: z .array(z.string()) .optional() - .default(['parameters-typescript', 'payloads-typescript']) + .default(['parameters-typescript', 'payloads-typescript', 'headers-typescript']) .describe('The list of other generator IDs that this generator depends on'), preset: z.literal('channels').default('channels'), outputPath: z @@ -57,6 +57,13 @@ export const zodTypescriptChannelsGenerator = z.object({ 'In case you have multiple TypeScript payload generators, you can specify which one to use as the dependency for this channels generator.' ) .default('payloads-typescript'), + headerGeneratorId: z + .string() + .optional() + .describe( + 'In case you have multiple TypeScript header generators, you can specify which one to use as the dependency for this channels generator.' + ) + .default('headers-typescript'), asyncapiReverseOperations: z .boolean() .optional() @@ -118,6 +125,7 @@ export interface TypeScriptChannelsGeneratorContext extends TypeScriptChannelsContext { payloads: TypeScriptPayloadRenderType; parameter: ConstrainedObjectModel | undefined; + headers: ConstrainedObjectModel | undefined; topic: string; subName: string; } @@ -144,6 +152,7 @@ export interface RenderRegularParameters { messageType: string; messageModule?: string; channelParameters: ConstrainedObjectModel | undefined; + channelHeaders: ConstrainedObjectModel | undefined; subName?: string; functionName?: string; payloadGenerator: TypeScriptPayloadRenderType; @@ -157,6 +166,7 @@ export interface RenderRequestReplyParameters { replyMessageType: string; replyMessageModule: string | undefined; channelParameters: ConstrainedObjectModel | undefined; + channelHeaders: ConstrainedObjectModel | undefined; subName?: string; functionName?: string; payloadGenerator: TypeScriptPayloadRenderType; diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index d679212c..136e9863 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -89,6 +89,29 @@ export function addParametersToExports( dependencies.push(`export {${parameter.modelName}};`); }); } + +export function addHeadersToDependencies( + headers: Record, + headerGenerator: {outputPath: string}, + currentGenerator: {outputPath: string}, + dependencies: string[] +) { + Object.values(headers) + .filter((model) => model !== undefined) + .forEach((header) => { + if (header === undefined) { + return; + } + const headerImportPath = path.relative( + currentGenerator.outputPath, + path.resolve(headerGenerator.outputPath, header.modelName) + ); + + dependencies.push( + `import {${header.modelName}} from './${ensureRelativePath(headerImportPath)}';` + ); + }); +} export function getMessageTypeAndModule(payload: ChannelPayload) { if (payload === undefined) { return {messageType: undefined, messageModule: undefined}; diff --git a/test/runtime/asyncapi-regular.json b/test/runtime/asyncapi-regular.json index d482e0c7..244b82e1 100644 --- a/test/runtime/asyncapi-regular.json +++ b/test/runtime/asyncapi-regular.json @@ -62,7 +62,7 @@ "$ref": "#/components/schemas/UserSignedUpPayload" }, "headers": { - "$ref": "#/components/schemas/UserSignedUpPayload" + "$ref": "#/components/schemas/UserSignedUpHeaders" } } }, @@ -80,6 +80,15 @@ "description": "Email of the user" } } + }, + "UserSignedUpHeaders": { + "type": "object", + "properties": { + "x-test-header": { + "type": "string", + "description": "Test header" + } + } } } } diff --git a/test/runtime/typescript/src/channels/index.ts b/test/runtime/typescript/src/channels/index.ts index 7d398c20..2bdb179a 100644 --- a/test/runtime/typescript/src/channels/index.ts +++ b/test/runtime/typescript/src/channels/index.ts @@ -1,5 +1,6 @@ import {UserSignedUp} from './../payloads/UserSignedUp'; import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as Nats from 'nats'; import * as Kafka from 'kafkajs'; import * as Mqtt from 'mqtt'; @@ -13,6 +14,7 @@ nats: { * * @param message to publish * @param parameters for topic substitution + * @param headers optional headers to include with the message * @param nc the NATS client to publish from * @param codec the serialization codec to use while transmitting the message * @param options to use while publishing the message @@ -20,12 +22,14 @@ nats: { publishToSendUserSignedup: ({ message, parameters, + headers, nc, codec = Nats.JSONCodec(), options }: { message: UserSignedUp, parameters: UserSignedupParameters, + headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions @@ -33,6 +37,18 @@ publishToSendUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } dataToSend = codec.encode(dataToSend); nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); @@ -46,6 +62,7 @@ nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{en * * @param message to publish over jetstream * @param parameters for topic substitution + * @param headers optional headers to include with the message * @param js the JetStream client to publish from * @param codec the serialization codec to use while transmitting the message * @param options to use while publishing the message @@ -53,12 +70,14 @@ nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{en jetStreamPublishToSendUserSignedup: ({ message, parameters, + headers, js, codec = Nats.JSONCodec(), options = {} }: { message: UserSignedUp, parameters: UserSignedupParameters, + headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial @@ -66,6 +85,18 @@ jetStreamPublishToSendUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } dataToSend = codec.encode(dataToSend); await js.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); @@ -81,6 +112,7 @@ await js.publish(parameters.getChannelWithParameters('user.signedup.{my_paramete * @param err if any error occurred this will be sat * @param msg that was received * @param parameters that was received in the topic + * @param headers that were received with the message * @param natsMsg */ @@ -102,7 +134,7 @@ subscribeToReceiveUserSignedup: ({ options, skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, natsMsg?: Nats.Msg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, parameters: UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -117,13 +149,22 @@ subscribeToReceiveUserSignedup: ({ for await (const msg of subscription) { const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of Array.from(msg.headers.entries())) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { - onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, parameters, msg); continue; + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, parameters, extractedHeaders, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); } })(); resolve(subscription); @@ -139,6 +180,7 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg) * @param err if any error occurred this will be sat * @param msg that was received * @param parameters that was received in the topic + * @param headers that was received with the message * @param jetstreamMsg */ @@ -160,7 +202,7 @@ jetStreamPullSubscribeToReceiveUserSignedup: ({ codec = Nats.JSONCodec(), skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, jetstreamMsg?: Nats.JsMsg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, parameters: UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, @@ -175,13 +217,22 @@ jetStreamPullSubscribeToReceiveUserSignedup: ({ for await (const msg of subscription) { const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, parameters, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); } })(); resolve(subscription); @@ -197,6 +248,7 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg) * @param err if any error occurred this will be sat * @param msg that was received * @param parameters that was received in the topic + * @param headers that was received with the message * @param jetstreamMsg */ @@ -218,7 +270,7 @@ jetStreamPushSubscriptionFromReceiveUserSignedup: ({ codec = Nats.JSONCodec(), skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, jetstreamMsg?: Nats.JsMsg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, parameters: UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, @@ -233,13 +285,22 @@ jetStreamPushSubscriptionFromReceiveUserSignedup: ({ for await (const msg of subscription) { const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, parameters, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); } })(); resolve(subscription); @@ -252,17 +313,20 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, msg) * NATS publish operation for `noparameters` * * @param message to publish + * @param headers optional headers to include with the message * @param nc the NATS client to publish from * @param codec the serialization codec to use while transmitting the message * @param options to use while publishing the message */ publishToNoParameter: ({ message, + headers, nc, codec = Nats.JSONCodec(), options }: { message: UserSignedUp, + headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions @@ -270,6 +334,18 @@ publishToNoParameter: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } dataToSend = codec.encode(dataToSend); nc.publish('noparameters', dataToSend, options); resolve(); @@ -284,6 +360,7 @@ nc.publish('noparameters', dataToSend, options); * @callback subscribeToNoParameterCallback * @param err if any error occurred this will be sat * @param msg that was received + * @param headers that were received with the message * @param natsMsg */ @@ -303,7 +380,7 @@ subscribeToNoParameter: ({ options, skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, natsMsg?: Nats.Msg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.SubscriptionOptions, @@ -317,13 +394,22 @@ subscribeToNoParameter: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of Array.from(msg.headers.entries())) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { - onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, extractedHeaders, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); } })(); resolve(subscription); @@ -338,6 +424,7 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); * @callback jetStreamPullSubscribeToNoParameterCallback * @param err if any error occurred this will be sat * @param msg that was received + * @param headers that was received with the message * @param jetstreamMsg */ @@ -357,7 +444,7 @@ jetStreamPullSubscribeToNoParameter: ({ codec = Nats.JSONCodec(), skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, jetstreamMsg?: Nats.JsMsg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -371,13 +458,22 @@ jetStreamPullSubscribeToNoParameter: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); } })(); resolve(subscription); @@ -392,6 +488,7 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); * @callback jetStreamPushSubscriptionFromNoParameterCallback * @param err if any error occurred this will be sat * @param msg that was received + * @param headers that was received with the message * @param jetstreamMsg */ @@ -411,7 +508,7 @@ jetStreamPushSubscriptionFromNoParameter: ({ codec = Nats.JSONCodec(), skipMessageValidation = false }: { - onDataCallback: (err?: Error, msg?: UserSignedUp, jetstreamMsg?: Nats.JsMsg) => void, + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -425,13 +522,22 @@ jetStreamPushSubscriptionFromNoParameter: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of msg.headers) { + headerObj[key] = value; + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } if(!skipMessageValidation) { const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); if(!valid) { onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; } } -onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); } })(); resolve(subscription); @@ -444,17 +550,20 @@ onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), msg); * JetStream publish operation for `noparameters` * * @param message to publish over jetstream + * @param headers optional headers to include with the message * @param js the JetStream client to publish from * @param codec the serialization codec to use while transmitting the message * @param options to use while publishing the message */ jetStreamPublishToNoParameter: ({ message, + headers, js, codec = Nats.JSONCodec(), options = {} }: { message: UserSignedUp, + headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial @@ -462,6 +571,18 @@ jetStreamPublishToNoParameter: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } dataToSend = codec.encode(dataToSend); await js.publish('noparameters', dataToSend, options); resolve(); diff --git a/test/runtime/typescript/src/headers/UserSignedUpHeaders.ts b/test/runtime/typescript/src/headers/UserSignedUpHeaders.ts index 1e42850d..d3baa43d 100644 --- a/test/runtime/typescript/src/headers/UserSignedUpHeaders.ts +++ b/test/runtime/typescript/src/headers/UserSignedUpHeaders.ts @@ -1,47 +1,35 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import addFormats from 'ajv-formats'; class UserSignedUpHeaders { - private _displayName?: string; - private _email?: string; + private _xTestHeader?: string; private _additionalProperties?: Map; constructor(input: { - displayName?: string, - email?: string, + xTestHeader?: string, additionalProperties?: Map, }) { - this._displayName = input.displayName; - this._email = input.email; + this._xTestHeader = input.xTestHeader; this._additionalProperties = input.additionalProperties; } /** - * Name of the user + * Test header */ - get displayName(): string | undefined { return this._displayName; } - set displayName(displayName: string | undefined) { this._displayName = displayName; } - - /** - * Email of the user - */ - get email(): string | undefined { return this._email; } - set email(email: string | undefined) { this._email = email; } + get xTestHeader(): string | undefined { return this._xTestHeader; } + set xTestHeader(xTestHeader: string | undefined) { this._xTestHeader = xTestHeader; } get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } public marshal() : string { let json = '{' - if(this.displayName !== undefined) { - json += `"display_name": ${typeof this.displayName === 'number' || typeof this.displayName === 'boolean' ? this.displayName : JSON.stringify(this.displayName)},`; - } - if(this.email !== undefined) { - json += `"email": ${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},`; + if(this.xTestHeader !== undefined) { + json += `"x-test-header": ${typeof this.xTestHeader === 'number' || typeof this.xTestHeader === 'boolean' ? this.xTestHeader : JSON.stringify(this.xTestHeader)},`; } if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object - if(["display_name","email","additionalProperties"].includes(String(key))) continue; + if(["x-test-header","additionalProperties"].includes(String(key))) continue; json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; } } @@ -53,21 +41,18 @@ class UserSignedUpHeaders { const obj = typeof json === "object" ? json : JSON.parse(json); const instance = new UserSignedUpHeaders({} as any); - if (obj["display_name"] !== undefined) { - instance.displayName = obj["display_name"]; - } - if (obj["email"] !== undefined) { - instance.email = obj["email"]; + if (obj["x-test-header"] !== undefined) { + instance.xTestHeader = obj["x-test-header"]; } instance.additionalProperties = new Map(); - const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["display_name","email","additionalProperties"].includes(key);})); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["x-test-header","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { instance.additionalProperties.set(key, value as any); } return instance; } - public static theCodeGenSchema = {"type":"object","properties":{"display_name":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user"}},"$id":"UserSignedUpHeaders","$schema":"http://json-schema.org/draft-07/schema"}; + public static theCodeGenSchema = {"type":"object","properties":{"x-test-header":{"type":"string","description":"Test header"}},"$id":"UserSignedUpHeaders","$schema":"http://json-schema.org/draft-07/schema"}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; const parsedData = typeof data === 'string' ? JSON.parse(data) : data; @@ -80,6 +65,7 @@ class UserSignedUpHeaders { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } diff --git a/test/runtime/typescript/src/payloads/UserSignedUp.ts b/test/runtime/typescript/src/payloads/UserSignedUp.ts index 4689be89..1ad4f615 100644 --- a/test/runtime/typescript/src/payloads/UserSignedUp.ts +++ b/test/runtime/typescript/src/payloads/UserSignedUp.ts @@ -74,6 +74,7 @@ class UserSignedUp { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } diff --git a/test/runtime/typescript/test/channels/regular/nats.spec.ts b/test/runtime/typescript/test/channels/regular/nats.spec.ts index 65ebd772..5ef06a57 100644 --- a/test/runtime/typescript/test/channels/regular/nats.spec.ts +++ b/test/runtime/typescript/test/channels/regular/nats.spec.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { AckPolicy, DeliverPolicy, JetStreamClient, JetStreamManager, NatsConnection, ReplayPolicy, ConsumerOpts, connect, JSONCodec } from "nats"; import { UserSignedUp, UserSignedupParameters } from '../../../src/client/NatsClient'; +import { UserSignedUpHeaders } from '../../../src/headers/UserSignedUpHeaders'; import { Protocols } from '../../../src/channels/index'; const { nats } = Protocols; const { @@ -10,6 +11,7 @@ const { describe('nats', () => { const testMessage = new UserSignedUp({ displayName: 'test', email: 'test@test.dk' }); const testParameters = new UserSignedupParameters({ myParameter: 'test', enumParameter: 'asyncapi' }); + const testHeaders = new UserSignedUpHeaders({ xTestHeader: 'test-header-value' }); describe('channels', () => { describe('with parameters', () => { @@ -45,6 +47,13 @@ describe('nats', () => { expect(msg.json()).toEqual("{\"display_name\": \"test\",\"email\": \"test@test.dk\"}"); }); + it('should be able publish over JetStream with headers', async () => { + await jetStreamPublishToSendUserSignedup({ message: testMessage, parameters: testParameters, headers: testHeaders, js }); + const msg = await jsm.streams.getMessage(test_stream, { last_by_subj: test_subj }); + expect(msg.json()).toEqual("{\"display_name\": \"test\",\"email\": \"test@test.dk\"}"); + expect(msg.headers?.get('x-test-header')).toEqual('test-header-value'); + }); + describe('should be able to do pull subscribe', () => { it('with correct payload', () => { const config = { @@ -60,11 +69,54 @@ describe('nats', () => { return new Promise(async (resolve, reject) => { js.publish(`user.signedup.${testParameters.myParameter}.${testParameters.enumParameter}`, testMessage.marshal()); const subscriber = await jetStreamPullSubscribeToReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters, jetstreamMsg) => { + onDataCallback: async (err, msg, parameters, headers, jetstreamMsg) => { + try { + expect(err).toBeUndefined(); + expect(msg?.marshal()).toEqual(testMessage.marshal()); + expect(parameters?.myParameter).toEqual(testParameters.myParameter); + expect(headers).toBeUndefined(); // No headers sent in this test + jetstreamMsg?.ack(); + await subscriber.drain(); + resolve(); + } catch (error) { + reject(error); + } + }, + parameters: new UserSignedupParameters({ myParameter: '*', enumParameter: 'asyncapi' }), + js, + options: config + }); + subscriber.pull({ batch: 1, expires: 10000 }); + }); + }); + + it('with correct payload and headers', () => { + const config = { + stream: test_stream, + config: { + durable_name: 'jps_correct_payload_headers', + ack_policy: AckPolicy.Explicit, + replay_policy: ReplayPolicy.Instant, + deliver_policy: DeliverPolicy.All, + }, + }; + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + // Publish with headers using JetStream + await jetStreamPublishToSendUserSignedup({ + message: testMessage, + parameters: testParameters, + headers: testHeaders, + js + }); + const subscriber = await jetStreamPullSubscribeToReceiveUserSignedup({ + onDataCallback: async (err, msg, parameters, headers, jetstreamMsg) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual(testMessage.marshal()); expect(parameters?.myParameter).toEqual(testParameters.myParameter); + expect(headers).toBeDefined(); + expect(headers?.xTestHeader).toEqual('test-header-value'); jetstreamMsg?.ack(); await subscriber.drain(); resolve(); @@ -130,7 +182,7 @@ describe('nats', () => { const incorrectPayload = JSON.stringify({ email: '123', displayName: 'test' }); js.publish(`user.signedup.${testParameters.myParameter}.${testParameters.enumParameter}`, incorrectPayload); const subscriber = await jetStreamPullSubscribeToReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters, jetstreamMsg) => { + onDataCallback: async (err, msg, parameters, headers, jetstreamMsg) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual("{\"email\": \"123\",\"displayName\": \"test\"}"); @@ -170,16 +222,36 @@ describe('nats', () => { }); }); + it('should be able to publish core with headers', () => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + nc.subscribe(test_subj, { + callback: async (err, msg) => { + try { + expect(err).toBeNull(); + expect(msg.json()).toEqual(testMessage.marshal()); + expect(msg.headers?.get('x-test-header')).toEqual('test-header-value'); + resolve(); + } catch (error) { + reject(error); + } + } + }); + await publishToSendUserSignedup({ message: testMessage, parameters: testParameters, headers: testHeaders, nc }); + }); + }); + describe('should be able to do core subscribe', () => { it('with correct payload', () => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const subscribtion = await subscribeToReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters) => { + onDataCallback: async (err, msg, parameters, headers) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual(testMessage.marshal()); expect(parameters?.myParameter).toEqual(testParameters.myParameter); + expect(headers).toBeUndefined(); // No headers sent in this test await subscribtion.drain(); resolve(); } catch (error) { @@ -192,11 +264,41 @@ describe('nats', () => { nc.publish(`user.signedup.${testParameters.myParameter}.${testParameters.enumParameter}`, testMessage.marshal()); }); }); + + it('with correct payload and headers', () => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const subscribtion = await subscribeToReceiveUserSignedup({ + onDataCallback: async (err, msg, parameters, headers) => { + try { + expect(err).toBeUndefined(); + expect(msg?.marshal()).toEqual(testMessage.marshal()); + expect(parameters?.myParameter).toEqual(testParameters.myParameter); + expect(headers).toBeDefined(); + expect(headers?.xTestHeader).toEqual('test-header-value'); + await subscribtion.drain(); + resolve(); + } catch (error) { + reject(error); + } + }, + parameters: new UserSignedupParameters({ myParameter: '*', enumParameter: 'asyncapi' }), + nc + }); + // Publish with headers using core publish + await publishToSendUserSignedup({ + message: testMessage, + parameters: testParameters, + headers: testHeaders, + nc + }); + }); + }); it('and catch incorrect payload', () => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const subscribtion = await subscribeToReceiveUserSignedup({ - onDataCallback: async (err, _, parameters) => { + onDataCallback: async (err, _, parameters, headers) => { try { expect(err).toBeDefined(); expect(err?.message).toBeDefined(); @@ -218,7 +320,7 @@ describe('nats', () => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const subscribtion = await subscribeToReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters) => { + onDataCallback: async (err, msg, parameters, headers) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual("{\"email\": \"123\",\"displayName\": \"test\"}"); @@ -253,7 +355,7 @@ describe('nats', () => { }; return new Promise(async (resolve, reject) => { const subscription = await jetStreamPushSubscriptionFromReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters, jetstreamMsg) => { + onDataCallback: async (err, msg, parameters, headers, jetstreamMsg) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual(testMessage.marshal()); @@ -317,7 +419,7 @@ describe('nats', () => { }; return new Promise(async (resolve, reject) => { const subscription = await jetStreamPushSubscriptionFromReceiveUserSignedup({ - onDataCallback: async (err, msg, parameters) => { + onDataCallback: async (err, msg, parameters, headers) => { try { expect(err).toBeUndefined(); expect(msg?.marshal()).toEqual("{\"email\": \"123\",\"displayName\": \"test\"}"); From 02dd14ecd50e70bebd3604b83a22ec24410d7f0e Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 6 Sep 2025 19:34:46 +0200 Subject: [PATCH 2/9] fix headers --- .../channels/protocols/nats/utils.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/codegen/generators/typescript/channels/protocols/nats/utils.ts diff --git a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts new file mode 100644 index 00000000..77436792 --- /dev/null +++ b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts @@ -0,0 +1,78 @@ +import { ConstrainedObjectModel } from '@asyncapi/modelina'; + +/** + * Generates the header setup code for NATS publish operations + */ +export function generateHeaderSetupCode(channelHeaders: ConstrainedObjectModel | undefined): string { + if (!channelHeaders) { + return ''; + } + + return `// Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + }`; +} + +/** + * Generates the header extraction code for NATS subscribe operations + */ +export function generateHeaderExtractionCode(channelHeaders: ConstrainedObjectModel | undefined): string { + if (!channelHeaders) { + return ''; + } + + return `// Extract headers if present + let extractedHeaders: ${channelHeaders.type} | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + for (const [key, value] of Array.from(msg.headers.entries())) { + headerObj[key] = value; + } + extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); + }`; +} + +/** + * Generates the function parameter for headers + */ +export function generateHeaderParameter(channelHeaders: ConstrainedObjectModel | undefined): { + parameter: string; + parameterType: string; + jsDoc: string; +} | null { + if (!channelHeaders) { + return null; + } + + return { + parameter: 'headers', + parameterType: `headers?: ${channelHeaders.type}`, + jsDoc: ' * @param headers optional headers to include with the message' + }; +} + +/** + * Generates the callback parameter for headers in subscribe functions + */ +export function generateHeaderCallbackParameter(channelHeaders: ConstrainedObjectModel | undefined): { + parameter: string; + jsDoc: string; +} | null { + if (!channelHeaders) { + return null; + } + + return { + parameter: `headers?: ${channelHeaders.type}`, + jsDoc: ' * @param headers that were received with the message' + }; +} From 2d55c0ad8f53cbbf56a82ea588bac49732c5f10a Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 6 Sep 2025 19:45:36 +0200 Subject: [PATCH 3/9] fix test --- .../typescript/__snapshots__/channels.spec.ts.snap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 840f7a8d..c24c926b 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -33,6 +33,7 @@ publishToUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = MessageTypeModule.marshal(message); + dataToSend = codec.encode(dataToSend); nc.publish('user.signedup', dataToSend, options); resolve(); @@ -210,6 +211,7 @@ jetStreamPublishToUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = MessageTypeModule.marshal(message); + dataToSend = codec.encode(dataToSend); await js.publish('user.signedup', dataToSend, options); resolve(); @@ -555,6 +557,7 @@ publishToUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = MessageTypeModule.marshal(message); + dataToSend = codec.encode(dataToSend); nc.publish('user.signedup', dataToSend, options); resolve(); @@ -732,6 +735,7 @@ jetStreamPublishToUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = MessageTypeModule.marshal(message); + dataToSend = codec.encode(dataToSend); await js.publish('user.signedup', dataToSend, options); resolve(); @@ -1072,6 +1076,7 @@ jetStreamPublishToUserSignedup: ({ return new Promise(async (resolve, reject) => { try { let dataToSend: any = MessageTypeModule.marshal(message); + dataToSend = codec.encode(dataToSend); await js.publish('user.signedup', dataToSend, options); resolve(); From 11c462fc72c4ae32bbca3d05b49357127297e893 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 6 Sep 2025 19:51:40 +0200 Subject: [PATCH 4/9] fix pr --- .../generators/typescript/channels/index.ts | 12 ++- .../channels/protocols/nats/coreSubscribe.ts | 66 +++++--------- .../protocols/nats/jetstreamPullSubscribe.ts | 62 +++----------- .../nats/jetstreamPushSubscription.ts | 61 +++---------- .../channels/protocols/nats/utils.ts | 85 +++++++++++++++++-- .../generators/typescript/channels/types.ts | 6 +- .../__snapshots__/channels.spec.ts.snap | 6 -- 7 files changed, 140 insertions(+), 158 deletions(-) diff --git a/src/codegen/generators/typescript/channels/index.ts b/src/codegen/generators/typescript/channels/index.ts index 15da459f..4d5b8b5e 100644 --- a/src/codegen/generators/typescript/channels/index.ts +++ b/src/codegen/generators/typescript/channels/index.ts @@ -25,7 +25,11 @@ import { zodTypescriptChannelsGenerator, ChannelFunctionTypes } from './types'; -import {addParametersToDependencies, addPayloadsToDependencies, addHeadersToDependencies} from './utils'; +import { + addParametersToDependencies, + addPayloadsToDependencies, + addHeadersToDependencies +} from './utils'; import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi'; export { TypeScriptChannelRenderedFunctionType, @@ -178,9 +182,9 @@ function validateContext(context: TypeScriptChannelsContext): { const parameters = context.dependencyOutputs[ generator.parameterGeneratorId ] as TypeScriptParameterRenderType; - const headers = context.dependencyOutputs[ - generator.headerGeneratorId - ] as TypeScriptHeadersRenderType | undefined; + const headers = context.dependencyOutputs[generator.headerGeneratorId] as + | TypeScriptHeadersRenderType + | undefined; if (!payloads) { throw new Error( 'Internal error, could not determine previous rendered payloads generator that is required for channel TypeScript generator' diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts index a038ffef..ab3eb6f0 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts @@ -5,7 +5,11 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {getValidationFunctions} from '../../utils'; -import {generateHeaderExtractionCode, generateHeaderCallbackParameter} from './utils'; +import { + generateHeaderExtractionCode, + generateHeaderCallbackParameter, + generateMessageReceivingCode +} from './utils'; export function renderCoreSubscribe({ topic, @@ -31,13 +35,14 @@ export function renderCoreSubscribe({ includeValidation, messageModule, messageType, - onValidationFail: channelParameters && channelHeaders - ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, extractedHeaders, msg); continue;` - : channelParameters - ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, msg); continue;` - : channelHeaders - ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, extractedHeaders, msg); continue;` - : `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, msg); continue;` + onValidationFail: + channelParameters && channelHeaders + ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, extractedHeaders, msg); continue;` + : channelParameters + ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, parameters, msg); continue;` + : channelHeaders + ? `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, extractedHeaders, msg); continue;` + : `onDataCallback(new Error(\`Invalid message payload received; $\{JSON.stringify({cause: errors})}\`), undefined, msg); continue;` }); messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -106,43 +111,14 @@ export function renderCoreSubscribe({ } ]; const headerExtraction = generateHeaderExtractionCode(channelHeaders); - - let whenReceivingMessage = ''; - if (channelParameters && channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; - } - } else if (channelParameters) { - if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; - } - } else if (channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; - } - } else if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, msg);`; - } + const whenReceivingMessage = generateMessageReceivingCode({ + channelParameters, + channelHeaders, + messageType, + messageUnmarshalling, + headerExtraction, + potentialValidationFunction + }); const jsDocParameters = functionParameters .map((param) => param.jsDoc) .join('\n'); diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts index b7b09b15..35878f38 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts @@ -5,7 +5,10 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {getValidationFunctions} from '../../utils'; -import {generateHeaderExtractionCode, generateHeaderCallbackParameter} from './utils'; +import { + generateHeaderExtractionCode, + generateMessageReceivingCode +} from './utils'; export function renderJetstreamPullSubscribe({ topic, @@ -105,54 +108,15 @@ export function renderJetstreamPullSubscribe({ } ]; - const headerExtraction = channelHeaders - ? `// Extract headers if present - let extractedHeaders: ${channelHeaders.type} | undefined = undefined; - if (msg.headers) { - const headerObj: Record = {}; - for (const [key, value] of msg.headers) { - headerObj[key] = value; - } - extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); - }` - : ''; - - let whenReceivingMessage = ''; - if (channelParameters && channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; - } - } else if (channelParameters) { - if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; - } - } else if (channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; - } - } else if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, msg);`; - } + const headerExtraction = generateHeaderExtractionCode(channelHeaders); + const whenReceivingMessage = generateMessageReceivingCode({ + channelParameters, + channelHeaders, + messageType, + messageUnmarshalling, + headerExtraction, + potentialValidationFunction + }); const jsDocParameters = functionParameters .map((param) => param.jsDoc) diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts index 0469ab69..fddeba2f 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts @@ -5,6 +5,10 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {getValidationFunctions} from '../../utils'; +import { + generateHeaderExtractionCode, + generateMessageReceivingCode +} from './utils'; export function renderJetstreamPushSubscription({ topic, @@ -107,54 +111,15 @@ export function renderJetstreamPushSubscription({ } ]; - const headerExtraction = channelHeaders - ? `// Extract headers if present - let extractedHeaders: ${channelHeaders.type} | undefined = undefined; - if (msg.headers) { - const headerObj: Record = {}; - for (const [key, value] of msg.headers) { - headerObj[key] = value; - } - extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); - }` - : ''; - - let whenReceivingMessage = ''; - if (channelParameters && channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, parameters, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, extractedHeaders, msg);`; - } - } else if (channelParameters) { - if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, parameters, msg);`; - } - } else if (channelHeaders) { - if (messageType === 'null') { - whenReceivingMessage = `${headerExtraction} - onDataCallback(undefined, null, extractedHeaders, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${headerExtraction} -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, extractedHeaders, msg);`; - } - } else if (messageType === 'null') { - whenReceivingMessage = `onDataCallback(undefined, null, msg);`; - } else { - whenReceivingMessage = `let receivedData: any = codec.decode(msg.data); -${potentialValidationFunction} -onDataCallback(undefined, ${messageUnmarshalling}, msg);`; - } + const headerExtraction = generateHeaderExtractionCode(channelHeaders); + const whenReceivingMessage = generateMessageReceivingCode({ + channelParameters, + channelHeaders, + messageType, + messageUnmarshalling, + headerExtraction, + potentialValidationFunction + }); const jsDocParameters = functionParameters .map((param) => param.jsDoc) diff --git a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts index 77436792..74efca36 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts @@ -1,9 +1,11 @@ -import { ConstrainedObjectModel } from '@asyncapi/modelina'; +import {ConstrainedObjectModel} from '@asyncapi/modelina'; /** * Generates the header setup code for NATS publish operations */ -export function generateHeaderSetupCode(channelHeaders: ConstrainedObjectModel | undefined): string { +export function generateHeaderSetupCode( + channelHeaders: ConstrainedObjectModel | undefined +): string { if (!channelHeaders) { return ''; } @@ -25,7 +27,9 @@ export function generateHeaderSetupCode(channelHeaders: ConstrainedObjectModel | /** * Generates the header extraction code for NATS subscribe operations */ -export function generateHeaderExtractionCode(channelHeaders: ConstrainedObjectModel | undefined): string { +export function generateHeaderExtractionCode( + channelHeaders: ConstrainedObjectModel | undefined +): string { if (!channelHeaders) { return ''; } @@ -44,7 +48,9 @@ export function generateHeaderExtractionCode(channelHeaders: ConstrainedObjectMo /** * Generates the function parameter for headers */ -export function generateHeaderParameter(channelHeaders: ConstrainedObjectModel | undefined): { +export function generateHeaderParameter( + channelHeaders: ConstrainedObjectModel | undefined +): { parameter: string; parameterType: string; jsDoc: string; @@ -63,7 +69,9 @@ export function generateHeaderParameter(channelHeaders: ConstrainedObjectModel | /** * Generates the callback parameter for headers in subscribe functions */ -export function generateHeaderCallbackParameter(channelHeaders: ConstrainedObjectModel | undefined): { +export function generateHeaderCallbackParameter( + channelHeaders: ConstrainedObjectModel | undefined +): { parameter: string; jsDoc: string; } | null { @@ -76,3 +84,70 @@ export function generateHeaderCallbackParameter(channelHeaders: ConstrainedObjec jsDoc: ' * @param headers that were received with the message' }; } + +/** + * Generates the message receiving code for NATS subscribe operations + */ +export function generateMessageReceivingCode({ + channelParameters, + channelHeaders, + messageType, + messageUnmarshalling, + headerExtraction, + potentialValidationFunction +}: { + channelParameters: ConstrainedObjectModel | undefined; + channelHeaders: ConstrainedObjectModel | undefined; + messageType: string; + messageUnmarshalling: string; + headerExtraction: string; + potentialValidationFunction: string; +}): string { + const hasParameters = !!channelParameters; + const hasHeaders = !!channelHeaders; + const isNullMessage = messageType === 'null'; + + // Build callback parameters + const callbackParams = ['undefined']; + + if (isNullMessage) { + callbackParams.push('null'); + } else { + callbackParams.push(messageUnmarshalling); + } + + if (hasParameters) { + callbackParams.push('parameters'); + } + + if (hasHeaders) { + callbackParams.push('extractedHeaders'); + } + + callbackParams.push('msg'); + + const callbackInvocation = `onDataCallback(${callbackParams.join(', ')});`; + + // Generate message processing code + if (isNullMessage) { + return hasHeaders + ? `${headerExtraction}\n ${callbackInvocation}` + : callbackInvocation; + } + + // Non-null message processing + const messageDecoding = 'let receivedData: any = codec.decode(msg.data);'; + const parts = [messageDecoding]; + + if (hasHeaders) { + parts.push(headerExtraction); + } + + if (potentialValidationFunction) { + parts.push(potentialValidationFunction); + } + + parts.push(callbackInvocation); + + return parts.join('\n'); +} diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index d7e904a5..9897812f 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -30,7 +30,11 @@ export const zodTypescriptChannelsGenerator = z.object({ dependencies: z .array(z.string()) .optional() - .default(['parameters-typescript', 'payloads-typescript', 'headers-typescript']) + .default([ + 'parameters-typescript', + 'payloads-typescript', + 'headers-typescript' + ]) .describe('The list of other generator IDs that this generator depends on'), preset: z.literal('channels').default('channels'), outputPath: z diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index c24c926b..4a2aaf0f 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -81,7 +81,6 @@ subscribeToUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); @@ -130,7 +129,6 @@ jetStreamPullSubscribeToUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); @@ -179,7 +177,6 @@ jetStreamPushSubscriptionFromUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); @@ -605,7 +602,6 @@ subscribeToUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); @@ -654,7 +650,6 @@ jetStreamPullSubscribeToUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); @@ -703,7 +698,6 @@ jetStreamPushSubscriptionFromUserSignedup: ({ for await (const msg of subscription) { let receivedData: any = codec.decode(msg.data); - onDataCallback(undefined, MessageTypeModule.unmarshal(receivedData), msg); } })(); From ab28be28942e2d2ef24c9229b230581156d0a036 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 6 Sep 2025 21:02:10 +0200 Subject: [PATCH 5/9] fix pr --- .../typescript/channels/asyncapi.ts | 9 +++--- .../generators/typescript/channels/index.ts | 31 ++++++++++++++++--- test/codegen/configurations.spec.ts | 6 ++-- .../generators/typescript/channels.spec.ts | 19 +++++++++--- test/runtime/typescript/src/channels/index.ts | 8 ++--- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/codegen/generators/typescript/channels/asyncapi.ts b/src/codegen/generators/typescript/channels/asyncapi.ts index e2b9be3c..073e0c07 100644 --- a/src/codegen/generators/typescript/channels/asyncapi.ts +++ b/src/codegen/generators/typescript/channels/asyncapi.ts @@ -92,7 +92,7 @@ export async function generateTypeScriptChannelsForAsyncAPI( context: TypeScriptChannelsContext, parameters: TypeScriptParameterRenderType, payloads: TypeScriptPayloadRenderType, - headers: TypeScriptHeadersRenderType | undefined, + headers: TypeScriptHeadersRenderType, protocolsToUse: SupportedProtocols[], protocolCodeFunctions: Record, externalProtocolFunctionInformation: Record< @@ -119,10 +119,9 @@ export async function generateTypeScriptChannelsForAsyncAPI( } } - let headerModel: OutputModel | undefined = undefined; - if (headers) { - headerModel = headers.channelModels[channel.id()]; - } + // Headers are now always available, but the specific channel may not have headers defined + const headerModel: OutputModel | undefined = + headers.channelModels[channel.id()]; for (const protocol of protocolsToUse) { const protocolContext: TypeScriptChannelsGeneratorContext = { diff --git a/src/codegen/generators/typescript/channels/index.ts b/src/codegen/generators/typescript/channels/index.ts index 4d5b8b5e..0e5fe1d9 100644 --- a/src/codegen/generators/typescript/channels/index.ts +++ b/src/codegen/generators/typescript/channels/index.ts @@ -14,7 +14,11 @@ import { TypeScriptPayloadRenderType, defaultTypeScriptPayloadGenerator } from '../payloads'; -import {TypeScriptHeadersRenderType} from '../headers'; +import { + TypeScriptHeadersRenderType, + TypescriptHeadersGenerator, + defaultTypeScriptHeadersOptions +} from '../headers'; import { TypeScriptChannelRenderedFunctionType, TypeScriptChannelRenderType, @@ -168,7 +172,7 @@ function addDependencies( function validateContext(context: TypeScriptChannelsContext): { payloads: TypeScriptPayloadRenderType; parameters: TypeScriptParameterRenderType; - headers: TypeScriptHeadersRenderType | undefined; + headers: TypeScriptHeadersRenderType; } { const {generator} = context; if (!context.dependencyOutputs) { @@ -182,9 +186,9 @@ function validateContext(context: TypeScriptChannelsContext): { const parameters = context.dependencyOutputs[ generator.parameterGeneratorId ] as TypeScriptParameterRenderType; - const headers = context.dependencyOutputs[generator.headerGeneratorId] as - | TypeScriptHeadersRenderType - | undefined; + const headers = context.dependencyOutputs[ + generator.headerGeneratorId + ] as TypeScriptHeadersRenderType; if (!payloads) { throw new Error( 'Internal error, could not determine previous rendered payloads generator that is required for channel TypeScript generator' @@ -195,6 +199,11 @@ function validateContext(context: TypeScriptChannelsContext): { 'Internal error, could not determine previous rendered parameters generator that is required for channel TypeScript generator' ); } + if (!headers) { + throw new Error( + 'Internal error, could not determine previous rendered headers generator that is required for channel TypeScript generator' + ); + } return {payloads, parameters, headers}; } @@ -208,6 +217,7 @@ export function includeTypeScriptChannelDependencies( const newGenerators: any[] = []; const parameterGeneratorId = generator.parameterGeneratorId; const payloadGeneratorId = generator.payloadGeneratorId; + const headerGeneratorId = generator.headerGeneratorId; const hasParameterGenerator = config.generators.find( (generatorSearch) => generatorSearch.id === parameterGeneratorId @@ -216,6 +226,10 @@ export function includeTypeScriptChannelDependencies( config.generators.find( (generatorSearch) => generatorSearch.id === payloadGeneratorId ) !== undefined; + const hasHeaderGenerator = + config.generators.find( + (generatorSearch) => generatorSearch.id === headerGeneratorId + ) !== undefined; if (!hasParameterGenerator) { const defaultChannelParameterGenerator: TypescriptParametersGenerator = { ...defaultTypeScriptParametersOptions, @@ -230,5 +244,12 @@ export function includeTypeScriptChannelDependencies( }; newGenerators.push(defaultChannelPayloadGenerator); } + if (!hasHeaderGenerator) { + const defaultChannelHeaderGenerator: TypescriptHeadersGenerator = { + ...defaultTypeScriptHeadersOptions, + outputPath: path.resolve(generator.outputPath ?? '', './headers') + }; + newGenerators.push(defaultChannelHeaderGenerator); + } return newGenerators; } diff --git a/test/codegen/configurations.spec.ts b/test/codegen/configurations.spec.ts index 3ce824ef..bf6b7f1b 100644 --- a/test/codegen/configurations.spec.ts +++ b/test/codegen/configurations.spec.ts @@ -129,7 +129,7 @@ describe('configuration manager', () => { ] }; const realizedConfiguration = realizeConfiguration(configuration); - expect(realizedConfiguration.generators.length).toEqual(3); + expect(realizedConfiguration.generators.length).toEqual(4); }); it('for client', async () => { const configuration: any = { @@ -145,7 +145,7 @@ describe('configuration manager', () => { ] }; const realizedConfiguration = realizeConfiguration(configuration); - expect(realizedConfiguration.generators.length).toEqual(4); + expect(realizedConfiguration.generators.length).toEqual(5); }); it('should overwrite protocols', async () => { const configuration: any = { @@ -161,7 +161,7 @@ describe('configuration manager', () => { ] }; const realizedConfiguration = realizeConfiguration(configuration); - expect(realizedConfiguration.generators.length).toEqual(3); + expect(realizedConfiguration.generators.length).toEqual(4); }); }); }); diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index 0d011eca..66e45487 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -8,12 +8,19 @@ jest.mock('node:fs/promises', () => ({ import fs from 'node:fs/promises'; import { ConstrainedAnyModel, ConstrainedObjectModel, OutputModel } from "@asyncapi/modelina"; import { TypeScriptPayloadRenderType } from "../../../../src/codegen/generators/typescript/payloads"; +import { TypeScriptHeadersRenderType } from "../../../../src/codegen/generators/typescript/headers"; describe('channels', () => { describe('typescript', () => { const payloadModel = new OutputModel('', new ConstrainedAnyModel('TestPayloadModel', undefined, {}, 'Payload'), 'TestPayloadModel', {models: {}, originalInput: undefined}, []); const parameterModel = new OutputModel('', new ConstrainedObjectModel('TestParameter', undefined, {}, 'Parameter', {}), 'TestParameter', {models: {}, originalInput: undefined}, []); + // Helper function to create mock headers dependency + const createHeadersDependency = (): TypeScriptHeadersRenderType => ({ + channelModels: {}, + generator: {outputPath: './test'} as any + }); + it('should work with basic AsyncAPI inputs', async () => { const parsedAsyncAPIDocument = await loadAsyncapiDocument(path.resolve(__dirname, '../../../configs/asyncapi.yaml')); const parametersDependency: TypeScriptParameterRenderType = { @@ -45,7 +52,8 @@ describe('channels', () => { asyncapiDocument: parsedAsyncAPIDocument, dependencyOutputs: { 'parameters-typescript': parametersDependency, - 'payloads-typescript': payloadsDependency + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() } }); expect(fs.writeFile).toHaveBeenCalled(); @@ -93,7 +101,8 @@ describe('channels', () => { asyncapiDocument: parsedAsyncAPIDocument, dependencyOutputs: { 'parameters-typescript': parametersDependency, - 'payloads-typescript': payloadsDependency + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() } }); expect(fs.writeFile).toHaveBeenCalled(); @@ -130,7 +139,8 @@ describe('channels', () => { asyncapiDocument: parsedAsyncAPIDocument, dependencyOutputs: { 'parameters-typescript': parametersDependency, - 'payloads-typescript': payloadsDependency + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() } }); expect(fs.writeFile).toHaveBeenCalled(); @@ -186,7 +196,8 @@ describe('channels', () => { asyncapiDocument: parsedAsyncAPIDocument, dependencyOutputs: { 'parameters-typescript': parametersDependency, - 'payloads-typescript': payloadsDependency + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() } }); expect(fs.writeFile).toHaveBeenCalled(); diff --git a/test/runtime/typescript/src/channels/index.ts b/test/runtime/typescript/src/channels/index.ts index 2bdb179a..3ab387cd 100644 --- a/test/runtime/typescript/src/channels/index.ts +++ b/test/runtime/typescript/src/channels/index.ts @@ -221,7 +221,7 @@ jetStreamPullSubscribeToReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of msg.headers) { + for (const [key, value] of Array.from(msg.headers.entries())) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -289,7 +289,7 @@ jetStreamPushSubscriptionFromReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of msg.headers) { + for (const [key, value] of Array.from(msg.headers.entries())) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -462,7 +462,7 @@ jetStreamPullSubscribeToNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of msg.headers) { + for (const [key, value] of Array.from(msg.headers.entries())) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -526,7 +526,7 @@ jetStreamPushSubscriptionFromNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of msg.headers) { + for (const [key, value] of Array.from(msg.headers.entries())) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); From 0504b4e3a6c4c10237edb302cd69236a388fddd1 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 7 Sep 2025 10:20:30 +0200 Subject: [PATCH 6/9] fix entries --- .../typescript/channels/protocols/nats/utils.ts | 2 +- test/runtime/typescript/src/channels/index.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts index 74efca36..8a3e7737 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts @@ -38,7 +38,7 @@ export function generateHeaderExtractionCode( let extractedHeaders: ${channelHeaders.type} | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); diff --git a/test/runtime/typescript/src/channels/index.ts b/test/runtime/typescript/src/channels/index.ts index 3ab387cd..d650f3f7 100644 --- a/test/runtime/typescript/src/channels/index.ts +++ b/test/runtime/typescript/src/channels/index.ts @@ -153,7 +153,7 @@ subscribeToReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -221,7 +221,7 @@ jetStreamPullSubscribeToReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -289,7 +289,7 @@ jetStreamPushSubscriptionFromReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -398,7 +398,7 @@ subscribeToNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -462,7 +462,7 @@ jetStreamPullSubscribeToNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); @@ -526,7 +526,7 @@ jetStreamPushSubscriptionFromNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Array.from(msg.headers.entries())) { + for (const [key, value] of Object.entries(msg.headers)) { headerObj[key] = value; } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); From 0ca9fda91eeb0fe5bfd4e37e69448b72f6e83bf5 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 7 Sep 2025 10:23:25 +0200 Subject: [PATCH 7/9] fix runtime testing --- .../channels/protocols/nats/utils.ts | 13 +++- test/runtime/typescript/src/channels/index.ts | 78 ++++++++++++++++--- test/runtime/typescript/test/headers.spec.ts | 5 +- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts index 8a3e7737..ac9086c0 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/utils.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/utils.ts @@ -38,8 +38,17 @@ export function generateHeaderExtractionCode( let extractedHeaders: ${channelHeaders.type} | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = ${channelHeaders.type}.unmarshal(headerObj); }`; diff --git a/test/runtime/typescript/src/channels/index.ts b/test/runtime/typescript/src/channels/index.ts index d650f3f7..b2ce4ff4 100644 --- a/test/runtime/typescript/src/channels/index.ts +++ b/test/runtime/typescript/src/channels/index.ts @@ -153,8 +153,17 @@ subscribeToReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } @@ -221,8 +230,17 @@ jetStreamPullSubscribeToReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } @@ -289,8 +307,17 @@ jetStreamPushSubscriptionFromReceiveUserSignedup: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } @@ -398,8 +425,17 @@ subscribeToNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } @@ -462,8 +498,17 @@ jetStreamPullSubscribeToNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } @@ -526,8 +571,17 @@ jetStreamPushSubscriptionFromNoParameter: ({ let extractedHeaders: UserSignedUpHeaders | undefined = undefined; if (msg.headers) { const headerObj: Record = {}; - for (const [key, value] of Object.entries(msg.headers)) { - headerObj[key] = value; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } } extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); } diff --git a/test/runtime/typescript/test/headers.spec.ts b/test/runtime/typescript/test/headers.spec.ts index 806a5cdf..48b097a6 100644 --- a/test/runtime/typescript/test/headers.spec.ts +++ b/test/runtime/typescript/test/headers.spec.ts @@ -2,12 +2,11 @@ import { UserSignedUpHeaders } from '../src/headers/UserSignedUpHeaders'; describe('headers', () => { const testObject = new UserSignedUpHeaders({ - email: 'emailTest', - displayName: 'displayNameTest' + xTestHeader: 'emailTest' }); test('be able to serialize model', () => { const serialized = testObject.marshal(); - expect(serialized).toEqual("{\"display_name\": \"displayNameTest\",\"email\": \"emailTest\"}"); + expect(serialized).toEqual("{\"x-test-header\": \"emailTest\"}"); }); test('be able to serialize model and turning it back to a model with the same values', () => { const serialized = testObject.marshal(); From 989dbbe269cc8bd6406569bf8f6f9530eb589eb3 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 7 Sep 2025 10:23:43 +0200 Subject: [PATCH 8/9] commit runtime test --- test/runtime/typescript/src/openapi/Types.ts | 24 + .../FindPetsByStatusAndCategoryHeaders.ts | 73 + .../FindPetsByStatusAndCategoryParameters.ts | 382 ++ .../src/openapi/parameters/Format.ts | 6 + .../src/openapi/parameters/SortBy.ts | 6 + .../src/openapi/parameters/SortOrder.ts | 6 + .../src/openapi/parameters/Status.ts | 6 + .../typescript/src/openapi/payloads/APet.ts | 144 + .../typescript/src/openapi/payloads/AUser.ts | 155 + .../openapi/payloads/AnUploadedResponse.ts | 95 + ...FindPetsByStatusAndCategoryResponse_200.ts | 3 + .../src/openapi/payloads/ItemStatus.ts | 7 + .../src/openapi/payloads/PetCategory.ts | 83 + .../src/openapi/payloads/PetOrder.ts | 132 + .../typescript/src/openapi/payloads/PetTag.ts | 83 + .../typescript/src/openapi/payloads/Status.ts | 7 + .../src/request-reply/channels/index.ts | 3172 +++++++++++++++++ .../src/request-reply/client/NatsClient.ts | 110 + .../MultiStatusResponseReplyPayload.ts | 49 + .../src/request-reply/payloads/NotFound.ts | 83 + .../src/request-reply/payloads/Ping.ts | 71 + .../src/request-reply/payloads/PingPayload.ts | 53 + .../src/request-reply/payloads/Pong.ts | 71 + 23 files changed, 4821 insertions(+) create mode 100644 test/runtime/typescript/src/openapi/Types.ts create mode 100644 test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts create mode 100644 test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts create mode 100644 test/runtime/typescript/src/openapi/parameters/Format.ts create mode 100644 test/runtime/typescript/src/openapi/parameters/SortBy.ts create mode 100644 test/runtime/typescript/src/openapi/parameters/SortOrder.ts create mode 100644 test/runtime/typescript/src/openapi/parameters/Status.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/APet.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/AUser.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/FindPetsByStatusAndCategoryResponse_200.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/ItemStatus.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/PetCategory.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/PetOrder.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/PetTag.ts create mode 100644 test/runtime/typescript/src/openapi/payloads/Status.ts create mode 100644 test/runtime/typescript/src/request-reply/channels/index.ts create mode 100644 test/runtime/typescript/src/request-reply/client/NatsClient.ts create mode 100644 test/runtime/typescript/src/request-reply/payloads/MultiStatusResponseReplyPayload.ts create mode 100644 test/runtime/typescript/src/request-reply/payloads/NotFound.ts create mode 100644 test/runtime/typescript/src/request-reply/payloads/Ping.ts create mode 100644 test/runtime/typescript/src/request-reply/payloads/PingPayload.ts create mode 100644 test/runtime/typescript/src/request-reply/payloads/Pong.ts diff --git a/test/runtime/typescript/src/openapi/Types.ts b/test/runtime/typescript/src/openapi/Types.ts new file mode 100644 index 00000000..b8efe5f7 --- /dev/null +++ b/test/runtime/typescript/src/openapi/Types.ts @@ -0,0 +1,24 @@ +export type Paths = '/pet' | '/pet/findByStatus/{status}/{categoryId}'; +export type OperationIds = 'addPet' | 'updatePet' | 'findPetsByStatusAndCategory'; +export function ToPath(operationId: OperationIds): Paths { + switch (operationId) { + case 'addPet': + return '/pet'; + case 'updatePet': + return '/pet'; + case 'findPetsByStatusAndCategory': + return '/pet/findByStatus/{status}/{categoryId}'; + default: + throw new Error('Unknown operation ID: ' + operationId); + } +} +export function ToOperationIds(path: Paths): OperationIds[] { + switch (path) { + case '/pet': + return ['addPet', 'updatePet']; + case '/pet/findByStatus/{status}/{categoryId}': + return ['findPetsByStatusAndCategory']; + default: + throw new Error('Unknown path: ' + path); + } +} diff --git a/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts b/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts new file mode 100644 index 00000000..b41ffc31 --- /dev/null +++ b/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts @@ -0,0 +1,73 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class FindPetsByStatusAndCategoryHeaders { + private _xRequestId?: string; + private _acceptLanguage?: string; + + constructor(input: { + xRequestId?: string, + acceptLanguage?: string, + }) { + this._xRequestId = input.xRequestId; + this._acceptLanguage = input.acceptLanguage; + } + + /** + * Unique request identifier for tracing + */ + get xRequestId(): string | undefined { return this._xRequestId; } + set xRequestId(xRequestId: string | undefined) { this._xRequestId = xRequestId; } + + /** + * Preferred language for response messages + */ + get acceptLanguage(): string | undefined { return this._acceptLanguage; } + set acceptLanguage(acceptLanguage: string | undefined) { this._acceptLanguage = acceptLanguage; } + + public marshal() : string { + let json = '{' + if(this.xRequestId !== undefined) { + json += `"X-Request-ID": ${typeof this.xRequestId === 'number' || typeof this.xRequestId === 'boolean' ? this.xRequestId : JSON.stringify(this.xRequestId)},`; + } + if(this.acceptLanguage !== undefined) { + json += `"Accept-Language": ${typeof this.acceptLanguage === 'number' || typeof this.acceptLanguage === 'boolean' ? this.acceptLanguage : JSON.stringify(this.acceptLanguage)},`; + } + + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): FindPetsByStatusAndCategoryHeaders { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new FindPetsByStatusAndCategoryHeaders({} as any); + + if (obj["X-Request-ID"] !== undefined) { + instance.xRequestId = obj["X-Request-ID"]; + } + if (obj["Accept-Language"] !== undefined) { + instance.acceptLanguage = obj["Accept-Language"]; + } + + + return instance; + } + public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"X-Request-ID":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Unique request identifier for tracing"},"Accept-Language":{"type":"string","pattern":"^[a-z]{2}(-[A-Z]{2})?$","default":"en-US","description":"Preferred language for response messages"}},"$id":"FindPetsByStatusAndCategoryHeaders","$schema":"http://json-schema.org/draft-07/schema"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { FindPetsByStatusAndCategoryHeaders }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts new file mode 100644 index 00000000..c29324d9 --- /dev/null +++ b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts @@ -0,0 +1,382 @@ +import {Status} from './Status'; +import {SortBy} from './SortBy'; +import {SortOrder} from './SortOrder'; +import {Format} from './Format'; +class FindPetsByStatusAndCategoryParameters { + private _status: Status; + private _categoryId: number; + private _limit?: number; + private _offset?: number; + private _sortBy?: SortBy; + private _sortOrder?: SortOrder; + private _tags?: string[]; + private _includePetDetails?: boolean; + private _format?: Format; + + constructor(input: { + status: Status, + categoryId: number, + limit?: number, + offset?: number, + sortBy?: SortBy, + sortOrder?: SortOrder, + tags?: string[], + includePetDetails?: boolean, + format?: Format, + }) { + this._status = input.status; + this._categoryId = input.categoryId; + this._limit = input.limit; + this._offset = input.offset; + this._sortBy = input.sortBy; + this._sortOrder = input.sortOrder; + this._tags = input.tags; + this._includePetDetails = input.includePetDetails; + this._format = input.format; + } + + /** + * Status value that needs to be considered for filter + */ + get status(): Status { return this._status; } + set status(status: Status) { this._status = status; } + + /** + * Category ID to filter pets by + */ + get categoryId(): number { return this._categoryId; } + set categoryId(categoryId: number) { this._categoryId = categoryId; } + + /** + * Maximum number of pets to return + */ + get limit(): number | undefined { return this._limit; } + set limit(limit: number | undefined) { this._limit = limit; } + + /** + * Number of pets to skip before returning results + */ + get offset(): number | undefined { return this._offset; } + set offset(offset: number | undefined) { this._offset = offset; } + + /** + * Sort pets by specified field + */ + get sortBy(): SortBy | undefined { return this._sortBy; } + set sortBy(sortBy: SortBy | undefined) { this._sortBy = sortBy; } + + /** + * Sort order for results + */ + get sortOrder(): SortOrder | undefined { return this._sortOrder; } + set sortOrder(sortOrder: SortOrder | undefined) { this._sortOrder = sortOrder; } + + /** + * Filter pets by tags (comma-separated) + */ + get tags(): string[] | undefined { return this._tags; } + set tags(tags: string[] | undefined) { this._tags = tags; } + + /** + * Include detailed pet information in response + */ + get includePetDetails(): boolean | undefined { return this._includePetDetails; } + set includePetDetails(includePetDetails: boolean | undefined) { this._includePetDetails = includePetDetails; } + + /** + * Response format preference + */ + get format(): Format | undefined { return this._format; } + set format(format: Format | undefined) { this._format = format; } + + + + /** + * Serialize path parameters according to OpenAPI 2.0/3.x specification + * @returns Record of parameter names to their serialized values for path substitution + */ + serializePathParameters(): Record { + const result: Record = {}; + + // Serialize path parameter: status (style: simple, explode: false) + if (this.status !== undefined && this.status !== null) { + const value = this.status; + if (Array.isArray(value)) { + result['status'] = value.map(val => encodeURIComponent(String(val))).join(','); + } else if (typeof value === 'object' && value !== null) { + result['status'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); + } else { + result['status'] = encodeURIComponent(String(value)); + } + } + // Serialize path parameter: categoryId (style: simple, explode: false) + if (this.categoryId !== undefined && this.categoryId !== null) { + const value = this.categoryId; + if (Array.isArray(value)) { + result['categoryId'] = value.map(val => encodeURIComponent(String(val))).join(','); + } else if (typeof value === 'object' && value !== null) { + result['categoryId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); + } else { + result['categoryId'] = encodeURIComponent(String(value)); + } + } + + return result; + } + /** + * Serialize query parameters according to OpenAPI 2.0/3.x specification + * @returns URLSearchParams object with serialized query parameters + */ + serializeQueryParameters(): URLSearchParams { + const params = new URLSearchParams(); + + // Serialize query parameter: limit (style: form, explode: true) + if (this.limit !== undefined && this.limit !== null) { + const value = this.limit; + if (Array.isArray(value)) { + value.forEach(val => params.append('limit', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('limit', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: offset (style: form, explode: true) + if (this.offset !== undefined && this.offset !== null) { + const value = this.offset; + if (Array.isArray(value)) { + value.forEach(val => params.append('offset', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('offset', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: sortBy (style: form, explode: true) + if (this.sortBy !== undefined && this.sortBy !== null) { + const value = this.sortBy; + if (Array.isArray(value)) { + value.forEach(val => params.append('sortBy', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('sortBy', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: sortOrder (style: form, explode: true) + if (this.sortOrder !== undefined && this.sortOrder !== null) { + const value = this.sortOrder; + if (Array.isArray(value)) { + value.forEach(val => params.append('sortOrder', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('sortOrder', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: tags (style: form, explode: false) + if (this.tags !== undefined && this.tags !== null) { + const value = this.tags; + if (Array.isArray(value)) { + params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); + } else if (typeof value === 'object' && value !== null) { + params.append('tags', Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(',')); + } else { + params.append('tags', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: includePetDetails (style: form, explode: true) + if (this.includePetDetails !== undefined && this.includePetDetails !== null) { + const value = this.includePetDetails; + if (Array.isArray(value)) { + value.forEach(val => params.append('includePetDetails', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('includePetDetails', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: format (style: form, explode: true) + if (this.format !== undefined && this.format !== null) { + const value = this.format; + if (Array.isArray(value)) { + value.forEach(val => params.append('format', String(val))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(key, String(val))); + } else { + params.append('format', String(value)); + } + } + + return params; + } + /** + * Get the complete serialized URL with path and query parameters + * @param basePath The base path template (e.g., '/users/{id}') + * @returns The complete URL with serialized parameters + */ + serializeUrl(basePath: string): string { + let url = basePath; + + // Replace path parameters + + const pathParams = this.serializePathParameters(); + for (const [name, value] of Object.entries(pathParams)) { + url = url.replace(new RegExp(`{${name}}`, 'g'), value); + } + + // Add query parameters + + const queryParams = this.serializeQueryParameters(); + const queryString = queryParams.toString(); + if (queryString) { + url += (url.includes('?') ? '&' : '?') + queryString; + } + + return url; + } + /** + * Deserialize URL and populate instance properties from query parameters + * @param url The URL to parse (can be full URL or just query string) + */ + deserializeUrl(url: string): void { + // Extract query string from URL + let queryString = ''; + if (url.includes('?')) { + queryString = url.split('?')[1]; + } else if (url.includes('=')) { + // Assume it's already a query string + queryString = url; + } + + if (!queryString) { + return; + } + + const params = new URLSearchParams(queryString); + + // Deserialize query parameter: limit (style: form, explode: true) + if (params.has('limit')) { + const value = params.get('limit'); + if (value) { + const decodedValue = decodeURIComponent(value); + const numValue = Number(decodedValue); + if (!isNaN(numValue)) { + this.limit = numValue; + } + } + } + // Deserialize query parameter: offset (style: form, explode: true) + if (params.has('offset')) { + const value = params.get('offset'); + if (value) { + const decodedValue = decodeURIComponent(value); + const numValue = Number(decodedValue); + if (!isNaN(numValue)) { + this.offset = numValue; + } + } + } + // Deserialize query parameter: sortBy (style: form, explode: true) + if (params.has('sortBy')) { + const value = params.get('sortBy'); + if (value) { + const decodedValue = decodeURIComponent(value); + this.sortBy = decodedValue as "name" | "id" | "category" | "status"; + } + } + // Deserialize query parameter: sortOrder (style: form, explode: true) + if (params.has('sortOrder')) { + const value = params.get('sortOrder'); + if (value) { + const decodedValue = decodeURIComponent(value); + this.sortOrder = decodedValue as "asc" | "desc"; + } + } + // Deserialize query parameter: tags (style: form, explode: false) + if (params.has('tags')) { + const value = params.get('tags'); + if (value === '') { + this.tags = []; + } else if (value) { + // Split by comma and decode + const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); + this.tags = decodedValues as string[]; + } + } + // Deserialize query parameter: includePetDetails (style: form, explode: true) + if (params.has('includePetDetails')) { + const value = params.get('includePetDetails'); + if (value) { + const decodedValue = decodeURIComponent(value); + this.includePetDetails = decodedValue.toLowerCase() === 'true'; + } + } + // Deserialize query parameter: format (style: form, explode: true) + if (params.has('format')) { + const value = params.get('format'); + if (value) { + const decodedValue = decodeURIComponent(value); + this.format = decodedValue as "json" | "xml" | "csv"; + } + } + } + + /** + * Static method to create a new instance from a URL + * @param url The URL to parse + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + + * @returns A new FindPetsByStatusAndCategoryParameters instance + */ + static fromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { + // Extract path parameters from URL + const pathParams = this.extractPathParameters(url, basePath); + const instance = new FindPetsByStatusAndCategoryParameters({ status: pathParams.status, categoryId: pathParams.categoryId }); + instance.deserializeUrl(url); + return instance; + } + + /** + * Extract path parameters from a URL using a base path template + * @param url The URL to extract parameters from + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns Object containing extracted path parameter values + */ + private static extractPathParameters(url: string, basePath: string): { status: "available" | "pending" | "sold", categoryId: number } { + // Remove query string from URL for path matching + const urlPath = url.split('?')[0]; + + // Create regex pattern from base path template + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + + const match = urlPath.match(regex); + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + } + + // Extract parameter names from base path template + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + + // Map matched values to parameter names + const result: any = {}; + paramNames.forEach((paramName, index) => { + const rawValue = match[index + 1]; + const decodeValue = decodeURIComponent(rawValue); + switch (paramName) { + case 'status': + result.status = decodeValue as "available" | "pending" | "sold"; + break; + case 'categoryId': + result.categoryId = Number(decodeValue) as number; + break; + default: + result[paramName] = decodeValue; + } + }); + + return result; + } +} +export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/Format.ts b/test/runtime/typescript/src/openapi/parameters/Format.ts new file mode 100644 index 00000000..53f45e9c --- /dev/null +++ b/test/runtime/typescript/src/openapi/parameters/Format.ts @@ -0,0 +1,6 @@ + +/** + * Response format preference + */ +type Format = "json" | "xml" | "csv"; +export { Format }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/SortBy.ts b/test/runtime/typescript/src/openapi/parameters/SortBy.ts new file mode 100644 index 00000000..b267a9d1 --- /dev/null +++ b/test/runtime/typescript/src/openapi/parameters/SortBy.ts @@ -0,0 +1,6 @@ + +/** + * Sort pets by specified field + */ +type SortBy = "name" | "id" | "category" | "status"; +export { SortBy }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/SortOrder.ts b/test/runtime/typescript/src/openapi/parameters/SortOrder.ts new file mode 100644 index 00000000..efb5a9d9 --- /dev/null +++ b/test/runtime/typescript/src/openapi/parameters/SortOrder.ts @@ -0,0 +1,6 @@ + +/** + * Sort order for results + */ +type SortOrder = "asc" | "desc"; +export { SortOrder }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/Status.ts b/test/runtime/typescript/src/openapi/parameters/Status.ts new file mode 100644 index 00000000..c82a0529 --- /dev/null +++ b/test/runtime/typescript/src/openapi/parameters/Status.ts @@ -0,0 +1,6 @@ + +/** + * Status value that needs to be considered for filter + */ +type Status = "available" | "pending" | "sold"; +export { Status }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/APet.ts b/test/runtime/typescript/src/openapi/payloads/APet.ts new file mode 100644 index 00000000..6cf026d2 --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/APet.ts @@ -0,0 +1,144 @@ +import {PetCategory} from './PetCategory'; +import {PetTag} from './PetTag'; +import {Status} from './Status'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class APet { + private _id?: number; + private _category?: PetCategory; + private _name: string; + private _photoUrls: string[]; + private _tags?: PetTag[]; + private _status?: Status; + private _additionalProperties?: Record; + + constructor(input: { + id?: number, + category?: PetCategory, + name: string, + photoUrls: string[], + tags?: PetTag[], + status?: Status, + additionalProperties?: Record, + }) { + this._id = input.id; + this._category = input.category; + this._name = input.name; + this._photoUrls = input.photoUrls; + this._tags = input.tags; + this._status = input.status; + this._additionalProperties = input.additionalProperties; + } + + get id(): number | undefined { return this._id; } + set id(id: number | undefined) { this._id = id; } + + get category(): PetCategory | undefined { return this._category; } + set category(category: PetCategory | undefined) { this._category = category; } + + get name(): string { return this._name; } + set name(name: string) { this._name = name; } + + get photoUrls(): string[] { return this._photoUrls; } + set photoUrls(photoUrls: string[]) { this._photoUrls = photoUrls; } + + get tags(): PetTag[] | undefined { return this._tags; } + set tags(tags: PetTag[] | undefined) { this._tags = tags; } + + get status(): Status | undefined { return this._status; } + set status(status: Status | undefined) { this._status = status; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.category !== undefined) { + json += `"category": ${this.category.marshal()},`; + } + if(this.name !== undefined) { + json += `"name": ${typeof this.name === 'number' || typeof this.name === 'boolean' ? this.name : JSON.stringify(this.name)},`; + } + if(this.photoUrls !== undefined) { + let photoUrlsJsonValues: any[] = []; + for (const unionItem of this.photoUrls) { + photoUrlsJsonValues.push(`${typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)}`); + } + json += `"photoUrls": [${photoUrlsJsonValues.join(',')}],`; + } + if(this.tags !== undefined) { + let tagsJsonValues: any[] = []; + for (const unionItem of this.tags) { + tagsJsonValues.push(`${unionItem.marshal()}`); + } + json += `"tags": [${tagsJsonValues.join(',')}],`; + } + if(this.status !== undefined) { + json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["id","category","name","photoUrls","tags","status","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): APet { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new APet({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["category"] !== undefined) { + instance.category = PetCategory.unmarshal(obj["category"]); + } + if (obj["name"] !== undefined) { + instance.name = obj["name"]; + } + if (obj["photoUrls"] !== undefined) { + instance.photoUrls = obj["photoUrls"]; + } + if (obj["tags"] !== undefined) { + instance.tags = obj["tags"] == null + ? null + : obj["tags"].map((item: any) => PetTag.unmarshal(item)); + } + if (obj["status"] !== undefined) { + instance.status = obj["status"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","category","name","photoUrls","tags","status","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"a Pet","description":"A pet for sale in the pet store","type":"object","required":["name","photoUrls"],"properties":{"id":{"type":"integer","format":"int64"},"category":{"title":"Pet category","description":"A category for a pet","type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string","pattern":"^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"}},"xml":{"name":"Category"}},"name":{"type":"string","example":"doggie"},"photoUrls":{"type":"array","xml":{"name":"photoUrl","wrapped":true},"items":{"type":"string"}},"tags":{"type":"array","xml":{"name":"tag","wrapped":true},"items":{"title":"Pet Tag","description":"A tag for a pet","type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"}}},"status":{"type":"string","description":"pet status in the store","deprecated":true,"enum":["available","pending","sold"]}},"xml":{"name":"Pet"},"$id":"Pet","$schema":"http://json-schema.org/draft-07/schema"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { APet }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/AUser.ts b/test/runtime/typescript/src/openapi/payloads/AUser.ts new file mode 100644 index 00000000..4aaafe88 --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/AUser.ts @@ -0,0 +1,155 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class AUser { + private _id?: number; + private _username?: string; + private _firstName?: string; + private _lastName?: string; + private _email?: string; + private _password?: string; + private _phone?: string; + private _userStatus?: number; + private _additionalProperties?: Record; + + constructor(input: { + id?: number, + username?: string, + firstName?: string, + lastName?: string, + email?: string, + password?: string, + phone?: string, + userStatus?: number, + additionalProperties?: Record, + }) { + this._id = input.id; + this._username = input.username; + this._firstName = input.firstName; + this._lastName = input.lastName; + this._email = input.email; + this._password = input.password; + this._phone = input.phone; + this._userStatus = input.userStatus; + this._additionalProperties = input.additionalProperties; + } + + get id(): number | undefined { return this._id; } + set id(id: number | undefined) { this._id = id; } + + get username(): string | undefined { return this._username; } + set username(username: string | undefined) { this._username = username; } + + get firstName(): string | undefined { return this._firstName; } + set firstName(firstName: string | undefined) { this._firstName = firstName; } + + get lastName(): string | undefined { return this._lastName; } + set lastName(lastName: string | undefined) { this._lastName = lastName; } + + get email(): string | undefined { return this._email; } + set email(email: string | undefined) { this._email = email; } + + get password(): string | undefined { return this._password; } + set password(password: string | undefined) { this._password = password; } + + get phone(): string | undefined { return this._phone; } + set phone(phone: string | undefined) { this._phone = phone; } + + get userStatus(): number | undefined { return this._userStatus; } + set userStatus(userStatus: number | undefined) { this._userStatus = userStatus; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.username !== undefined) { + json += `"username": ${typeof this.username === 'number' || typeof this.username === 'boolean' ? this.username : JSON.stringify(this.username)},`; + } + if(this.firstName !== undefined) { + json += `"firstName": ${typeof this.firstName === 'number' || typeof this.firstName === 'boolean' ? this.firstName : JSON.stringify(this.firstName)},`; + } + if(this.lastName !== undefined) { + json += `"lastName": ${typeof this.lastName === 'number' || typeof this.lastName === 'boolean' ? this.lastName : JSON.stringify(this.lastName)},`; + } + if(this.email !== undefined) { + json += `"email": ${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},`; + } + if(this.password !== undefined) { + json += `"password": ${typeof this.password === 'number' || typeof this.password === 'boolean' ? this.password : JSON.stringify(this.password)},`; + } + if(this.phone !== undefined) { + json += `"phone": ${typeof this.phone === 'number' || typeof this.phone === 'boolean' ? this.phone : JSON.stringify(this.phone)},`; + } + if(this.userStatus !== undefined) { + json += `"userStatus": ${typeof this.userStatus === 'number' || typeof this.userStatus === 'boolean' ? this.userStatus : JSON.stringify(this.userStatus)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["id","username","firstName","lastName","email","password","phone","userStatus","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): AUser { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new AUser({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["username"] !== undefined) { + instance.username = obj["username"]; + } + if (obj["firstName"] !== undefined) { + instance.firstName = obj["firstName"]; + } + if (obj["lastName"] !== undefined) { + instance.lastName = obj["lastName"]; + } + if (obj["email"] !== undefined) { + instance.email = obj["email"]; + } + if (obj["password"] !== undefined) { + instance.password = obj["password"]; + } + if (obj["phone"] !== undefined) { + instance.phone = obj["phone"]; + } + if (obj["userStatus"] !== undefined) { + instance.userStatus = obj["userStatus"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","username","firstName","lastName","email","password","phone","userStatus","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"a User","description":"A User who is purchasing from the pet store","type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"integer","format":"int32","description":"User Status"}},"xml":{"name":"User"},"$id":"User","$schema":"http://json-schema.org/draft-07/schema"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { AUser }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts b/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts new file mode 100644 index 00000000..1733bcbc --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts @@ -0,0 +1,95 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class AnUploadedResponse { + private _code?: number; + private _type?: string; + private _message?: string; + private _additionalProperties?: Record; + + constructor(input: { + code?: number, + type?: string, + message?: string, + additionalProperties?: Record, + }) { + this._code = input.code; + this._type = input.type; + this._message = input.message; + this._additionalProperties = input.additionalProperties; + } + + get code(): number | undefined { return this._code; } + set code(code: number | undefined) { this._code = code; } + + get type(): string | undefined { return this._type; } + set type(type: string | undefined) { this._type = type; } + + get message(): string | undefined { return this._message; } + set message(message: string | undefined) { this._message = message; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.code !== undefined) { + json += `"code": ${typeof this.code === 'number' || typeof this.code === 'boolean' ? this.code : JSON.stringify(this.code)},`; + } + if(this.type !== undefined) { + json += `"type": ${typeof this.type === 'number' || typeof this.type === 'boolean' ? this.type : JSON.stringify(this.type)},`; + } + if(this.message !== undefined) { + json += `"message": ${typeof this.message === 'number' || typeof this.message === 'boolean' ? this.message : JSON.stringify(this.message)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["code","type","message","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): AnUploadedResponse { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new AnUploadedResponse({} as any); + + if (obj["code"] !== undefined) { + instance.code = obj["code"]; + } + if (obj["type"] !== undefined) { + instance.type = obj["type"]; + } + if (obj["message"] !== undefined) { + instance.message = obj["message"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["code","type","message","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"An uploaded response","description":"Describes the result of uploading an image resource","type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}},"$id":"ApiResponse","$schema":"http://json-schema.org/draft-07/schema"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { AnUploadedResponse }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/FindPetsByStatusAndCategoryResponse_200.ts b/test/runtime/typescript/src/openapi/payloads/FindPetsByStatusAndCategoryResponse_200.ts new file mode 100644 index 00000000..1c6e70d4 --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/FindPetsByStatusAndCategoryResponse_200.ts @@ -0,0 +1,3 @@ +import {APet} from './APet'; +type FindPetsByStatusAndCategoryResponse_200 = APet[]; +export { FindPetsByStatusAndCategoryResponse_200 }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/ItemStatus.ts b/test/runtime/typescript/src/openapi/payloads/ItemStatus.ts new file mode 100644 index 00000000..d99fea16 --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/ItemStatus.ts @@ -0,0 +1,7 @@ + +enum ItemStatus { + AVAILABLE = "available", + PENDING = "pending", + SOLD = "sold", +} +export { ItemStatus }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetCategory.ts b/test/runtime/typescript/src/openapi/payloads/PetCategory.ts new file mode 100644 index 00000000..d0499bce --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/PetCategory.ts @@ -0,0 +1,83 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class PetCategory { + private _id?: number; + private _name?: string; + private _additionalProperties?: Record; + + constructor(input: { + id?: number, + name?: string, + additionalProperties?: Record, + }) { + this._id = input.id; + this._name = input.name; + this._additionalProperties = input.additionalProperties; + } + + get id(): number | undefined { return this._id; } + set id(id: number | undefined) { this._id = id; } + + get name(): string | undefined { return this._name; } + set name(name: string | undefined) { this._name = name; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.name !== undefined) { + json += `"name": ${typeof this.name === 'number' || typeof this.name === 'boolean' ? this.name : JSON.stringify(this.name)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["id","name","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): PetCategory { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new PetCategory({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["name"] !== undefined) { + instance.name = obj["name"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","name","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"Pet category","description":"A category for a pet","type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string","pattern":"^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"}},"xml":{"name":"Category"}}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { PetCategory }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetOrder.ts b/test/runtime/typescript/src/openapi/payloads/PetOrder.ts new file mode 100644 index 00000000..23e1f6fa --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/PetOrder.ts @@ -0,0 +1,132 @@ +import {Status} from './Status'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class PetOrder { + private _id?: number; + private _petId?: number; + private _quantity?: number; + private _shipDate?: string; + private _status?: Status; + private _complete?: boolean; + private _additionalProperties?: Record; + + constructor(input: { + id?: number, + petId?: number, + quantity?: number, + shipDate?: string, + status?: Status, + complete?: boolean, + additionalProperties?: Record, + }) { + this._id = input.id; + this._petId = input.petId; + this._quantity = input.quantity; + this._shipDate = input.shipDate; + this._status = input.status; + this._complete = input.complete; + this._additionalProperties = input.additionalProperties; + } + + get id(): number | undefined { return this._id; } + set id(id: number | undefined) { this._id = id; } + + get petId(): number | undefined { return this._petId; } + set petId(petId: number | undefined) { this._petId = petId; } + + get quantity(): number | undefined { return this._quantity; } + set quantity(quantity: number | undefined) { this._quantity = quantity; } + + get shipDate(): string | undefined { return this._shipDate; } + set shipDate(shipDate: string | undefined) { this._shipDate = shipDate; } + + get status(): Status | undefined { return this._status; } + set status(status: Status | undefined) { this._status = status; } + + get complete(): boolean | undefined { return this._complete; } + set complete(complete: boolean | undefined) { this._complete = complete; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.petId !== undefined) { + json += `"petId": ${typeof this.petId === 'number' || typeof this.petId === 'boolean' ? this.petId : JSON.stringify(this.petId)},`; + } + if(this.quantity !== undefined) { + json += `"quantity": ${typeof this.quantity === 'number' || typeof this.quantity === 'boolean' ? this.quantity : JSON.stringify(this.quantity)},`; + } + if(this.shipDate !== undefined) { + json += `"shipDate": ${typeof this.shipDate === 'number' || typeof this.shipDate === 'boolean' ? this.shipDate : JSON.stringify(this.shipDate)},`; + } + if(this.status !== undefined) { + json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + } + if(this.complete !== undefined) { + json += `"complete": ${typeof this.complete === 'number' || typeof this.complete === 'boolean' ? this.complete : JSON.stringify(this.complete)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["id","petId","quantity","shipDate","status","complete","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): PetOrder { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new PetOrder({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["petId"] !== undefined) { + instance.petId = obj["petId"]; + } + if (obj["quantity"] !== undefined) { + instance.quantity = obj["quantity"]; + } + if (obj["shipDate"] !== undefined) { + instance.shipDate = obj["shipDate"]; + } + if (obj["status"] !== undefined) { + instance.status = obj["status"]; + } + if (obj["complete"] !== undefined) { + instance.complete = obj["complete"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","petId","quantity","shipDate","status","complete","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"Pet Order","description":"An order for a pets from the pet store","type":"object","properties":{"id":{"type":"integer","format":"int64"},"petId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean","default":false}},"xml":{"name":"Order"},"$id":"Order","$schema":"http://json-schema.org/draft-07/schema"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { PetOrder }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetTag.ts b/test/runtime/typescript/src/openapi/payloads/PetTag.ts new file mode 100644 index 00000000..90859573 --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/PetTag.ts @@ -0,0 +1,83 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class PetTag { + private _id?: number; + private _name?: string; + private _additionalProperties?: Record; + + constructor(input: { + id?: number, + name?: string, + additionalProperties?: Record, + }) { + this._id = input.id; + this._name = input.name; + this._additionalProperties = input.additionalProperties; + } + + get id(): number | undefined { return this._id; } + set id(id: number | undefined) { this._id = id; } + + get name(): string | undefined { return this._name; } + set name(name: string | undefined) { this._name = name; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.name !== undefined) { + json += `"name": ${typeof this.name === 'number' || typeof this.name === 'boolean' ? this.name : JSON.stringify(this.name)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["id","name","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): PetTag { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new PetTag({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["name"] !== undefined) { + instance.name = obj["name"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","name","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"title":"Pet Tag","description":"A tag for a pet","type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"}}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + ajvInstance.addVocabulary(["xml", "example"]) + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { PetTag }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/Status.ts b/test/runtime/typescript/src/openapi/payloads/Status.ts new file mode 100644 index 00000000..a7f0745e --- /dev/null +++ b/test/runtime/typescript/src/openapi/payloads/Status.ts @@ -0,0 +1,7 @@ + +enum Status { + AVAILABLE = "available", + PENDING = "pending", + SOLD = "sold", +} +export { Status }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/channels/index.ts b/test/runtime/typescript/src/request-reply/channels/index.ts new file mode 100644 index 00000000..540a87b2 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/channels/index.ts @@ -0,0 +1,3172 @@ +import {Pong} from './../payloads/Pong'; +import {Ping} from './../payloads/Ping'; +import * as MultiStatusResponseReplyPayloadModule from './../payloads/MultiStatusResponseReplyPayload'; +import * as PingPayloadModule from './../payloads/PingPayload'; +import {NotFound} from './../payloads/NotFound'; +import * as Nats from 'nats'; +import { URLSearchParams, URL } from 'url'; +import * as NodeFetch from 'node-fetch'; +export const Protocols = { +nats: { + /** + * Core request for `ping` + * + * @param requestMessage the message to send + * @param nc the nats client to setup the request for + * @param codec the serialization codec to use when transmitting request and receiving reply + * @param options when sending the request + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +requestToRegularRequest: ({ + requestMessage, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + requestMessage: Ping, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.RequestOptions, + skipMessageValidation?: boolean +}): Promise => { + return new Promise(async (resolve, reject) => { + try { + const validator = Pong.createValidator(); + let dataToSend: any = requestMessage.marshal(); + dataToSend = codec.encode(dataToSend); + + const msg = await nc.request('ping', dataToSend, options); + const receivedData: any = codec.decode(msg.data); + if(!skipMessageValidation) { + const {valid, errors} = Pong.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + return reject(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`)); + } + } + resolve(Pong.unmarshal(receivedData)); + } catch (e: any) { + reject(e); + } + }); +}, +/** + * Callback for when receiving the request + * + * @callback replyToRegularReplyCallback + * @param err if any error occurred this will be sat + * @param requestMessage that was received from the request + */ + +/** + * Reply for `ping` + * + * @param {replyToRegularReplyCallback} onDataCallback to call when the request is received + * @param nc the nats client to setup the reply for + * @param codec the serialization codec to use when receiving request and transmitting reply + * @param options when setting up the reply + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +replyToRegularReply: ({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, requestMessage?: Pong) => Ping | Promise, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise => { + return new Promise(async (resolve, reject) => { + try { + let subscription = nc.subscribe('ping', options); + const validator = Pong.createValidator(); + (async () => { + for await (const msg of subscription) { + + + let receivedData : any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = Pong.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error('Invalid request payload received', JSON.stringify({cause: errors})), undefined); continue; + } + } +const replyMessage = await onDataCallback(undefined, Pong.unmarshal(receivedData) ); + + if (msg.reply) { + let dataToSend : any = replyMessage.marshal(); +dataToSend = codec.encode(dataToSend); +msg.respond(dataToSend); + } else { + onDataCallback(new Error('Expected request to need a reply, did not..')) + } + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} +}, +http_client: { + async postPingPostRequest(context: { + server?: string; + payload: Ping; + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + if (parsedContext.payload) { + body = parsedContext.payload.marshal(); + } + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'POST', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "POST", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "POST", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "POST", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async getPingGetRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'GET', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async putPingPutRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'PUT', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PUT", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PUT", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PUT", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async deletePingDeleteRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'DELETE', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "DELETE", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "DELETE", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "DELETE", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async patchPingPatchRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'PATCH', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PATCH", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PATCH", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "PATCH", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async headPingHeadRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'HEAD', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "HEAD", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "HEAD", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "HEAD", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async optionsPingOptionsRequest(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'OPTIONS', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "OPTIONS", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "OPTIONS", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "OPTIONS", + headers, + body + }); + + const data = await retryResponse.json(); + return Pong.unmarshal(data); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + // Handle common HTTP error codes with standardized messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } + + const data = await response.json(); + return Pong.unmarshal(data); +}, +async getMultiStatusResponse(context: { + server?: string; + + path?: string; + bearerToken?: string; + username?: string; + password?: string; + apiKey?: string; // API key value + apiKeyName?: string; // Name of the API key parameter + apiKeyIn?: 'header' | 'query'; // Where to place the API key (default: header) + // OAuth2 parameters + oauth2?: { + clientId: string; + clientSecret?: string; + accessToken?: string; + refreshToken?: string; + tokenUrl?: string; + authorizationUrl?: string; + redirectUri?: string; + scopes?: string[]; + flow?: 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; // Added flow parameter + // For password flow + username?: string; // Username for password flow + password?: string; // Password for password flow + onTokenRefresh?: (newTokens: { + accessToken: string; + refreshToken?: string; + expiresIn?: number; + }) => void; + // For Implicit flow + responseType?: 'token' | 'id_token' | 'id_token token'; // For Implicit flow + state?: string; // For security against CSRF + onImplicitRedirect?: (authUrl: string) => void; // Callback for handling the redirect in Implicit flow + }; + credentials?: RequestCredentials; //value for the credentials param we want to use on each request + additionalHeaders?: Record; //header params we want to use on every request, + makeRequestCallback?: ({ + method, body, url, headers + }: { + url: string, + headers?: Record, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD', + credentials?: RequestCredentials, + body?: any + }) => Promise<{ + ok: boolean, + status: number, + statusText: string, + json: () => Record | Promise>, + }> + }): Promise { + const parsedContext = { + ...{ + makeRequestCallback: async ({url, body, method, headers}) => { + return NodeFetch.default(url, { + body, + method, + headers + }) + }, + path: '/ping', + server: 'localhost:3000', + apiKeyIn: 'header', + apiKeyName: 'X-API-Key', + }, + ...context, + } + + // Validate parameters before proceeding with the request + // OAuth2 Implicit flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit') { + if (!parsedContext.oauth2.authorizationUrl) { + return Promise.reject(new Error('OAuth2 Implicit flow requires authorizationUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Implicit flow requires clientId')); + } + if (!parsedContext.oauth2.redirectUri) { + return Promise.reject(new Error('OAuth2 Implicit flow requires redirectUri')); + } + if (!parsedContext.oauth2.onImplicitRedirect) { + return Promise.reject(new Error('OAuth2 Implicit flow requires onImplicitRedirect handler')); + } + } + + // OAuth2 Client Credentials flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Client Credentials flow requires clientId')); + } + } + + // OAuth2 Password flow validation + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password') { + if (!parsedContext.oauth2.tokenUrl) { + return Promise.reject(new Error('OAuth2 Password flow requires tokenUrl')); + } + if (!parsedContext.oauth2.clientId) { + return Promise.reject(new Error('OAuth2 Password flow requires clientId')); + } + if (!parsedContext.oauth2.username) { + return Promise.reject(new Error('OAuth2 Password flow requires username')); + } + if (!parsedContext.oauth2.password) { + return Promise.reject(new Error('OAuth2 Password flow requires password')); + } + } + + const headers = { + 'Content-Type': 'application/json', + ...parsedContext.additionalHeaders + }; + let url = `${parsedContext.server}${parsedContext.path}`; + + let body: any; + + + // Handle different authentication methods + if (parsedContext.oauth2 && parsedContext.oauth2.accessToken) { + // OAuth2 authentication with existing access token + headers["Authorization"] = `Bearer ${parsedContext.oauth2.accessToken}`; + } else if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'implicit' && parsedContext.oauth2.authorizationUrl && parsedContext.oauth2.onImplicitRedirect) { + // Build the authorization URL for implicit flow + const authUrl = new URL(parsedContext.oauth2.authorizationUrl); + authUrl.searchParams.append('client_id', parsedContext.oauth2.clientId); + authUrl.searchParams.append('redirect_uri', parsedContext.oauth2.redirectUri!); + authUrl.searchParams.append('response_type', parsedContext.oauth2.responseType || 'token'); + + if (parsedContext.oauth2.state) { + authUrl.searchParams.append('state', parsedContext.oauth2.state); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + authUrl.searchParams.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Call the redirect handler + parsedContext.oauth2.onImplicitRedirect(authUrl.toString()); + // Since we've initiated a redirect flow, we can't continue with the request + // The application will need to handle the redirect and subsequent token extraction + return Promise.reject(new Error('OAuth2 Implicit flow redirect initiated')); + } else if (parsedContext.bearerToken) { + // bearer authentication + headers["Authorization"] = `Bearer ${parsedContext.bearerToken}`; + } else if (parsedContext.username && parsedContext.password) { + // basic authentication + const credentials = Buffer.from(`${parsedContext.username}:${parsedContext.password}`).toString('base64'); + headers["Authorization"] = `Basic ${credentials}`; + } + + // API Key Authentication + if (parsedContext.apiKey) { + if (parsedContext.apiKeyIn === 'header') { + // Add API key to headers + headers[parsedContext.apiKeyName] = parsedContext.apiKey; + } else if (parsedContext.apiKeyIn === 'query') { + // Add API key to query parameters + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${parsedContext.apiKeyName}=${encodeURIComponent(parsedContext.apiKey)}`; + } + } + + // Make the API request + const response = await parsedContext.makeRequestCallback({url, + method: 'GET', + headers, + body + }); + + // Handle OAuth2 Client Credentials flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'client_credentials' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: parsedContext.oauth2.clientId + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + // Some APIs use basic auth with client credentials instead of form params + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // If both client ID and secret are provided, some servers prefer basic auth + if (parsedContext.oauth2.clientId && parsedContext.oauth2.clientSecret) { + const credentials = Buffer.from( + `${parsedContext.oauth2.clientId}:${parsedContext.oauth2.clientSecret}` + ).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + // Remove client_id and client_secret from the request body when using basic auth + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return MultiStatusResponseReplyPayloadModule.unmarshalByStatusCode(data, retryResponse.status); + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 Client Credentials flow:', error); + return Promise.reject(error); + } + } + + // Handle OAuth2 password flow + if (parsedContext.oauth2 && parsedContext.oauth2.flow === 'password' && parsedContext.oauth2.tokenUrl) { + try { + const params = new URLSearchParams({ + grant_type: 'password', + username: parsedContext.oauth2.username || '', + password: parsedContext.oauth2.password || '', + client_id: parsedContext.oauth2.clientId, + }); + + if (parsedContext.oauth2.clientSecret) { + params.append('client_secret', parsedContext.oauth2.clientSecret); + } + + if (parsedContext.oauth2.scopes && parsedContext.oauth2.scopes.length > 0) { + params.append('scope', parsedContext.oauth2.scopes.join(' ')); + } + + const tokenResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: params.toString() + }); + + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + const tokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Update headers with the new token + headers["Authorization"] = `Bearer ${tokens.accessToken}`; + + // Notify the client about the tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return MultiStatusResponseReplyPayloadModule.unmarshalByStatusCode(data, retryResponse.status); + + } else { + return Promise.reject(new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`)); + } + } catch (error) { + console.error('Error in OAuth2 password flow:', error); + return Promise.reject(error); + } + } + + // Handle token refresh for OAuth2 if we get a 401 + if (response.status === 401 && parsedContext.oauth2 && parsedContext.oauth2.refreshToken && parsedContext.oauth2.tokenUrl && parsedContext.oauth2.clientId) { + try { + const refreshResponse = await NodeFetch.default(parsedContext.oauth2.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: parsedContext.oauth2.refreshToken, + client_id: parsedContext.oauth2.clientId, + ...(parsedContext.oauth2.clientSecret ? { client_secret: parsedContext.oauth2.clientSecret } : {}) + }).toString() + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + const newTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || parsedContext.oauth2.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Update the access token for this request + headers["Authorization"] = `Bearer ${newTokens.accessToken}`; + + // Notify the client about the refreshed tokens + if (parsedContext.oauth2.onTokenRefresh) { + parsedContext.oauth2.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const retryResponse = await parsedContext.makeRequestCallback({ + url, + method: "GET", + headers, + body + }); + + const data = await retryResponse.json(); + return MultiStatusResponseReplyPayloadModule.unmarshalByStatusCode(data, retryResponse.status); + } else { + // Token refresh failed, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } catch (error) { + console.error('Error refreshing token:', error); + // For any error during refresh, return a standardized error message + return Promise.reject(new Error('Unauthorized')); + } + } + + // Handle error status codes before attempting to parse JSON + if (!response.ok) { + // For multi-status responses (with replyMessageModule), let unmarshalByStatusCode handle the parsing + // Only throw standardized errors for simple responses or when JSON parsing fails + + } + + // For multi-status responses, always try to parse JSON and let unmarshalByStatusCode handle it + try { + const data = await response.json(); + return MultiStatusResponseReplyPayloadModule.unmarshalByStatusCode(data, response.status); + } catch (error) { + // If JSON parsing fails or unmarshalByStatusCode fails, provide standardized error messages + if (response.status === 401) { + return Promise.reject(new Error('Unauthorized')); + } else if (response.status === 403) { + return Promise.reject(new Error('Forbidden')); + } else if (response.status === 404) { + return Promise.reject(new Error('Not Found')); + } else if (response.status === 500) { + return Promise.reject(new Error('Internal Server Error')); + } else { + return Promise.reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); + } + } +} +}}; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/client/NatsClient.ts b/test/runtime/typescript/src/request-reply/client/NatsClient.ts new file mode 100644 index 00000000..16da0ee5 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/client/NatsClient.ts @@ -0,0 +1,110 @@ +import {Pong} from './../payloads/Pong'; +import {Ping} from './../payloads/Ping'; +import * as MultiStatusResponseReplyPayloadModule from './../payloads/MultiStatusResponseReplyPayload'; +import * as PingPayloadModule from './../payloads/PingPayload'; +import {NotFound} from './../payloads/NotFound'; +export {Pong}; +export {Ping}; +export {MultiStatusResponseReplyPayloadModule}; +export {PingPayloadModule}; +export {NotFound}; + +//Import channel functions +import { Protocols } from './../channels/index'; +const { nats } = Protocols; + +import * as Nats from 'nats'; + +/** + * @class NatsClient + */ +export class NatsClient { + public nc?: Nats.NatsConnection; + public js?: Nats.JetStreamClient; + public codec?: Nats.Codec; + public options?: Nats.ConnectionOptions; + + /** + * Disconnect all clients from the server and drain subscriptions. + * + * This is a gentle disconnect and make take a little. + */ + async disconnect() { + if (!this.isClosed() && this.nc !== undefined) { + await this.nc.drain(); + } + } + /** + * Returns whether or not any of the clients are closed + */ + isClosed() { + if (!this.nc || this.nc.isClosed()) { + return true; + } + return false; + } + /** + * Try to connect to the NATS server with user credentials + * + * @param userCreds to use + * @param options to connect with + */ + async connectWithUserCreds(userCreds: string, options?: Nats.ConnectionOptions, codec?: Nats.Codec ) { + return await this.connect({ + user: userCreds, + ...options + }, codec); + } + /** + * Try to connect to the NATS server with user and password + * + * @param user username to use + * @param pass password to use + * @param options to connect with + */ + async connectWithUserPass(user: string, pass: string, options?: Nats.ConnectionOptions, codec?: Nats.Codec ) { + return await this.connect({ + user: user, + pass: pass, + ...options + }, codec); + } + /** + * Try to connect to the NATS server which has no authentication + + * @param host to connect to + * @param options to connect with + */ + async connectToHost(host: string, options?: Nats.ConnectionOptions, codec?: Nats.Codec ) { + return await this.connect({ + servers: [host], + ...options + }, codec); + } + + /** + * Try to connect to the NATS server with the different payloads. + * @param options to use, payload is omitted if sat in the AsyncAPI document. + */ + connect(options: Nats.ConnectionOptions, codec?: Nats.Codec): Promise<{nc: Nats.NatsConnection, js: Nats.JetStreamClient}> { + return new Promise(async (resolve, reject: (error: any) => void) => { + if (!this.isClosed()) { + return reject('Client is still connected, please close it first.'); + } + this.options = options; + if (codec) { + this.codec = codec; + } else { + this.codec = Nats.JSONCodec(); + } + try { + this.nc = await Nats.connect(this.options); + this.js = this.nc.jetstream(); + resolve({nc: this.nc, js: this.js}); + } catch (e: any) { + reject('Could not connect to NATS server'); + } + }) + } + +} \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/MultiStatusResponseReplyPayload.ts b/test/runtime/typescript/src/request-reply/payloads/MultiStatusResponseReplyPayload.ts new file mode 100644 index 00000000..b3afa687 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/payloads/MultiStatusResponseReplyPayload.ts @@ -0,0 +1,49 @@ +import {Pong} from './Pong'; +import {NotFound} from './NotFound'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +type MultiStatusResponseReplyPayload = Pong | NotFound; + +export function unmarshal(json: any): MultiStatusResponseReplyPayload { + + return JSON.parse(json); +} +export function marshal(payload: MultiStatusResponseReplyPayload) { + if(payload instanceof Pong) { +return payload.marshal(); +} +if(payload instanceof NotFound) { +return payload.marshal(); +} + return JSON.stringify(payload); +} + +export function unmarshalByStatusCode(json: any, statusCode: number): MultiStatusResponseReplyPayload { + if (statusCode === 200) { + return Pong.unmarshal(json); + } + if (statusCode === 404) { + return NotFound.unmarshal(json); + } + throw new Error(`No matching type found for status code: ${statusCode}`); +} +export const theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","properties":{"pong":{"type":"string","description":"pong name"}},"$id":"pong"},{"type":"object","properties":{"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Error code"}},"$id":"notFound"}],"$id":"MultiStatusResponseReplyPayload"}; +export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + +export { MultiStatusResponseReplyPayload }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/NotFound.ts b/test/runtime/typescript/src/request-reply/payloads/NotFound.ts new file mode 100644 index 00000000..e845b836 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/payloads/NotFound.ts @@ -0,0 +1,83 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class NotFound { + private _error?: string; + private _code?: string; + private _additionalProperties?: Record; + + constructor(input: { + error?: string, + code?: string, + additionalProperties?: Record, + }) { + this._error = input.error; + this._code = input.code; + this._additionalProperties = input.additionalProperties; + } + + get error(): string | undefined { return this._error; } + set error(error: string | undefined) { this._error = error; } + + get code(): string | undefined { return this._code; } + set code(code: string | undefined) { this._code = code; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.error !== undefined) { + json += `"error": ${typeof this.error === 'number' || typeof this.error === 'boolean' ? this.error : JSON.stringify(this.error)},`; + } + if(this.code !== undefined) { + json += `"code": ${typeof this.code === 'number' || typeof this.code === 'boolean' ? this.code : JSON.stringify(this.code)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["error","code","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): NotFound { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new NotFound({} as any); + + if (obj["error"] !== undefined) { + instance.error = obj["error"]; + } + if (obj["code"] !== undefined) { + instance.code = obj["code"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["error","code","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","properties":{"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Error code"}},"$id":"notFound"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { NotFound }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/Ping.ts b/test/runtime/typescript/src/request-reply/payloads/Ping.ts new file mode 100644 index 00000000..abf94274 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/payloads/Ping.ts @@ -0,0 +1,71 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class Ping { + private _ping?: string; + private _additionalProperties?: Record; + + constructor(input: { + ping?: string, + additionalProperties?: Record, + }) { + this._ping = input.ping; + this._additionalProperties = input.additionalProperties; + } + + get ping(): string | undefined { return this._ping; } + set ping(ping: string | undefined) { this._ping = ping; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.ping !== undefined) { + json += `"ping": ${typeof this.ping === 'number' || typeof this.ping === 'boolean' ? this.ping : JSON.stringify(this.ping)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["ping","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): Ping { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new Ping({} as any); + + if (obj["ping"] !== undefined) { + instance.ping = obj["ping"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["ping","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","properties":{"ping":{"type":"string","description":"ping name"}},"$id":"ping"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { Ping }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/PingPayload.ts b/test/runtime/typescript/src/request-reply/payloads/PingPayload.ts new file mode 100644 index 00000000..e8c62509 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/payloads/PingPayload.ts @@ -0,0 +1,53 @@ +import {Ping} from './Ping'; +import {Pong} from './Pong'; +import {NotFound} from './NotFound'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +type PingPayload = Ping | Pong | NotFound; + +export function unmarshal(json: any): PingPayload { + + return JSON.parse(json); +} +export function marshal(payload: PingPayload) { + if(payload instanceof Ping) { +return payload.marshal(); +} +if(payload instanceof Pong) { +return payload.marshal(); +} +if(payload instanceof NotFound) { +return payload.marshal(); +} + return JSON.stringify(payload); +} + +export function unmarshalByStatusCode(json: any, statusCode: number): PingPayload { + if (statusCode === 200) { + return Pong.unmarshal(json); + } + if (statusCode === 404) { + return NotFound.unmarshal(json); + } + throw new Error(`No matching type found for status code: ${statusCode}`); +} +export const theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","properties":{"ping":{"type":"string","description":"ping name"}},"$id":"ping"},{"type":"object","properties":{"pong":{"type":"string","description":"pong name"}},"$id":"pong"},{"type":"object","properties":{"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Error code"}},"$id":"notFound"}],"$id":"PingPayload"}; +export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + +export { PingPayload }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/Pong.ts b/test/runtime/typescript/src/request-reply/payloads/Pong.ts new file mode 100644 index 00000000..e57f7316 --- /dev/null +++ b/test/runtime/typescript/src/request-reply/payloads/Pong.ts @@ -0,0 +1,71 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +class Pong { + private _pong?: string; + private _additionalProperties?: Record; + + constructor(input: { + pong?: string, + additionalProperties?: Record, + }) { + this._pong = input.pong; + this._additionalProperties = input.additionalProperties; + } + + get pong(): string | undefined { return this._pong; } + set pong(pong: string | undefined) { this._pong = pong; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.pong !== undefined) { + json += `"pong": ${typeof this.pong === 'number' || typeof this.pong === 'boolean' ? this.pong : JSON.stringify(this.pong)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["pong","additionalProperties"].includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): Pong { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new Pong({} as any); + + if (obj["pong"] !== undefined) { + instance.pong = obj["pong"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["pong","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","properties":{"pong":{"type":"string","description":"pong name"}},"$id":"pong"}; + public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? this.createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; + } + public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { Pong }; \ No newline at end of file From 8dbec6ba826196c3ca013d359ee5042dea5ba71c Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 7 Sep 2025 10:27:06 +0200 Subject: [PATCH 9/9] fix headers test --- test/runtime/typescript/test/channels/regular/nats.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtime/typescript/test/channels/regular/nats.spec.ts b/test/runtime/typescript/test/channels/regular/nats.spec.ts index 5ef06a57..75f93179 100644 --- a/test/runtime/typescript/test/channels/regular/nats.spec.ts +++ b/test/runtime/typescript/test/channels/regular/nats.spec.ts @@ -51,7 +51,7 @@ describe('nats', () => { await jetStreamPublishToSendUserSignedup({ message: testMessage, parameters: testParameters, headers: testHeaders, js }); const msg = await jsm.streams.getMessage(test_stream, { last_by_subj: test_subj }); expect(msg.json()).toEqual("{\"display_name\": \"test\",\"email\": \"test@test.dk\"}"); - expect(msg.headers?.get('x-test-header')).toEqual('test-header-value'); + expect(msg.header.get('x-test-header')).toEqual('test-header-value'); }); describe('should be able to do pull subscribe', () => {