From ad383ad8fbe7b2873bb6f4a98cae61a5452b1b8d Mon Sep 17 00:00:00 2001 From: Jack Stevenson Date: Thu, 28 Nov 2024 07:43:01 +1030 Subject: [PATCH] feat(type-safe-api): add metadata option to typescript codegen for esm compatible code (#888) * feat(type-safe-api): add metadata option to typescript codegen for esm compatible code Adds an option to the typescript generators to emit esm compatible code (eg imports ending with .js). * fix(type-safe-api): fix overwrite when output dir is not working directory --- .../type-safe-api/generators/generate-next.ts | 2 +- .../templates/api.ejs | 8 + .../templates/functions.ejs | 15 +- .../templates/index.ejs | 6 +- .../templates/tests.ejs | 4 +- .../templates/index.ejs | 8 +- .../templates/server/interceptors.ejs | 26 +- .../templates/server/operationConfig.ejs | 4 +- .../templates/server/serverSdk.ejs | 2 +- .../templates/api.ejs | 8 + .../templates/functions.ejs | 7 +- .../templates/index.ejs | 6 +- .../templates/mockIntegrations.ejs | 13 +- .../templates/tests.ejs | 4 +- .../templates/client.ejs | 2 +- .../templates/index.ejs | 4 +- .../typescript/templates/client/apis/apis.ejs | 6 +- .../templates/client/apis/index.ejs | 2 +- .../templates/client/models/index.ejs | 2 +- .../templates/client/models/models.ejs | 4 +- .../generators/typescript/templates/index.ejs | 12 +- .../templates/server/interceptors.ejs | 30 +- .../templates/server/operationConfig.ejs | 4 +- .../typescript/templates/server/response.ejs | 2 +- ...typescript-cdk-infrastructure.test.ts.snap | 12 +- .../__snapshots__/typescript-esm.test.ts.snap | 4620 +++++++++++++++++ .../scripts/generators/typescript-esm.test.ts | 58 + 27 files changed, 4795 insertions(+), 76 deletions(-) create mode 100644 packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-esm.test.ts.snap create mode 100644 packages/type-safe-api/test/scripts/generators/typescript-esm.test.ts diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/generate-next.ts b/packages/type-safe-api/scripts/type-safe-api/generators/generate-next.ts index 54b410c3c..81794a273 100755 --- a/packages/type-safe-api/scripts/type-safe-api/generators/generate-next.ts +++ b/packages/type-safe-api/scripts/type-safe-api/generators/generate-next.ts @@ -266,7 +266,7 @@ const splitAndWriteFiles = (renderedFileContents: string[], outputPath: string) splitFiles.push({ contents: newFileContents, pathRelativeToOutputPath: newFilePath, - shouldWrite: !fs.existsSync(newFilePath) || config.overwrite, + shouldWrite: !fs.existsSync(path.join(outputPath, newFilePath)) || config.overwrite, config, }); })); diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/api.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/api.ejs index e0769978d..02f9f1034 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/api.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/api.ejs @@ -9,7 +9,11 @@ ###/TSAPI_WRITE_FILE###import { TypeSafeWebsocketApi, TypeSafeWebsocketApiProps, TypeSafeWebsocketApiIntegration } from "@aws/pdk/type-safe-api"; import { Construct } from "constructs"; import { OperationConfig, OperationLookup } from "<%- metadata.runtimePackageName %>"; +<%_ if (metadata.esm) { _%> +import * as url from 'url'; +<%_ } else { _%> import * as path from "path"; +<%_ } _%> export type WebSocketApiIntegrations = OperationConfig; @@ -27,7 +31,11 @@ export class WebSocketApi extends TypeSafeWebsocketApi { ...props, integrations: props.integrations as any, operationLookup: OperationLookup, + <%_ if (metadata.esm) { _%> + specPath: url.fileURLToPath(new URL("<%- metadata.relativeSpecPath %>", import.meta.url)), + <%_ } else { _%> specPath: path.resolve(__dirname, "<%- metadata.relativeSpecPath %>"), + <%_ } _%> }); } } diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/functions.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/functions.ejs index f6957cd74..2bd82b005 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/functions.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/functions.ejs @@ -10,6 +10,9 @@ import { Duration } from "aws-cdk-lib"; import { SnapStartFunction, SnapStartFunctionProps } from "@aws/pdk/type-safe-api"; import { Code, Function, Runtime, Tracing, FunctionProps } from "aws-cdk-lib/aws-lambda"; import * as path from "path"; +<%_ if (metadata.esm) { _%> +import * as url from 'url'; +<%_ } _%> <%_ if (vendorExtensions['x-connect-handler']) { _%> <%_ const language = vendorExtensions['x-connect-handler'].language; _%> @@ -41,7 +44,7 @@ export class $ConnectFunction extends <% if (isJava) { %>SnapStart<% } %>Functio <%_ } else if (isJava) { _%> handler: "<%- metadata['x-handlers-java-package'] %>.$ConnectHandler", <%_ } _%> - code: Code.fromAsset(path.resolve(__dirname, "..", + code: Code.fromAsset(<%_ if (metadata.esm) { _%>url.fileURLToPath(new URL(path.join("..",<%_ } else { _%>path.resolve(__dirname, "..",<%_ } %> <%_ if (isTypeScript) { _%> "<%- metadata['x-handlers-typescript-asset-path'] %>", "$connect", @@ -50,7 +53,7 @@ export class $ConnectFunction extends <% if (isJava) { %>SnapStart<% } %>Functio <%_ } else if (isJava) { _%> "<%- metadata['x-handlers-java-asset-path'] %>", <%_ } _%> - )), + )<%_ if (metadata.esm) { _%>, import.meta.url))<%_ } _%>), tracing: Tracing.ACTIVE, timeout: Duration.seconds(30), ...props, @@ -89,7 +92,7 @@ export class $DisconnectFunction extends <% if (isJava) { %>SnapStart<% } %>Func <%_ } else if (isJava) { _%> handler: "<%- metadata['x-handlers-java-package'] %>.$DisconnectHandler", <%_ } _%> - code: Code.fromAsset(path.resolve(__dirname, "..", + code: Code.fromAsset(<%_ if (metadata.esm) { _%>url.fileURLToPath(new URL(path.join("..",<%_ } else { _%>path.resolve(__dirname, "..",<%_ } %> <%_ if (isTypeScript) { _%> "<%- metadata['x-handlers-typescript-asset-path'] %>", "$disconnect", @@ -98,7 +101,7 @@ export class $DisconnectFunction extends <% if (isJava) { %>SnapStart<% } %>Func <%_ } else if (isJava) { _%> "<%- metadata['x-handlers-java-asset-path'] %>", <%_ } _%> - )), + )<%_ if (metadata.esm) { _%>, import.meta.url))<%_ } _%>), tracing: Tracing.ACTIVE, timeout: Duration.seconds(30), ...props, @@ -138,7 +141,7 @@ export class <%- operation.operationIdPascalCase %>Function extends <% if (isJav <%_ } else if (isJava) { _%> handler: "<%- metadata['x-handlers-java-package'] %>.<%- operation.operationIdPascalCase %>Handler", <%_ } _%> - code: Code.fromAsset(path.resolve(__dirname, "..", + code: Code.fromAsset(<%_ if (metadata.esm) { _%>url.fileURLToPath(new URL(path.join("..",<%_ } else { _%>path.resolve(__dirname, "..",<%_ } %> <%_ if (isTypeScript) { _%> "<%- metadata['x-handlers-typescript-asset-path'] %>", "<%- operation.operationIdKebabCase %>", @@ -147,7 +150,7 @@ export class <%- operation.operationIdPascalCase %>Function extends <% if (isJav <%_ } else if (isJava) { _%> "<%- metadata['x-handlers-java-asset-path'] %>", <%_ } _%> - )), + )<%_ if (metadata.esm) { _%>, import.meta.url))<%_ } _%>), tracing: Tracing.ACTIVE, timeout: Duration.seconds(30), ...props, diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/index.ejs index 94d055e7c..a1b9bda92 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-cdk-infrastructure/templates/index.ejs @@ -6,6 +6,6 @@ "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###export * from "./api"; -export * from "./functions"; -export * from "./mock-integrations"; \ No newline at end of file +###/TSAPI_WRITE_FILE###export * from "./api<%_ if (metadata.esm) { _%>.js<%_ } _%>"; +export * from "./functions<%_ if (metadata.esm) { _%>.js<%_ } _%>"; +export * from "./mock-integrations<%_ if (metadata.esm) { _%>.js<%_ } _%>"; \ No newline at end of file diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-lambda-handlers/templates/tests.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-lambda-handlers/templates/tests.ejs index 4c388ea6c..7b58464e1 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-lambda-handlers/templates/tests.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-lambda-handlers/templates/tests.ejs @@ -13,7 +13,7 @@ } from "<%= metadata.runtimePackageName %>"; import { <%- operation.name %> -} from "../src/<%- operation.operationIdKebabCase %>"; +} from "../src/<%- operation.operationIdKebabCase %><%_ if (metadata.esm) { _%>.js<%_ } _%>"; // Common request arguments const requestArguments = { @@ -24,7 +24,7 @@ const requestArguments = { context: {} as any, interceptorContext: { logger: { - info: jest.fn(), + info: <% if (metadata.vitest) { %>vi<% } else { %>jest<% } %>.fn(), }, }, } satisfies Omit<<%- operation.operationIdPascalCase %>ChainedRequestInput, 'input'>; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/index.ejs index dfcded83b..6bf3bb91e 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/index.ejs @@ -5,7 +5,7 @@ "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###export * from './models'; -export * from './server/operation-config'; -export * from './server/server-sdk'; -export * from './interceptors' +###/TSAPI_WRITE_FILE###export * from './models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; +export * from './server/operation-config<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './server/server-sdk<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './interceptors<%_ if (metadata.esm) { _%>/index.js<%_ } _%>' diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/interceptors.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/interceptors.ejs index 5678c8f9e..4a859600c 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/interceptors.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/interceptors.ejs @@ -7,7 +7,7 @@ } ###/TSAPI_WRITE_FILE###import { PayloadlessChainedRequestInput, -} from '..'; +} from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; /** * Create an interceptor which catches any unhandled exceptions @@ -39,7 +39,7 @@ export const tryCatchInterceptor = buildTryCatchInterceptor(); "overwrite": true } ###/TSAPI_WRITE_FILE###import { Logger } from '@aws-lambda-powertools/logger'; -import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const logger = new Logger(); @@ -78,7 +78,7 @@ export class LoggingInterceptor { "overwrite": true } ###/TSAPI_WRITE_FILE###import { Tracer } from '@aws-lambda-powertools/tracer'; -import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const tracer = new Tracer(); @@ -149,7 +149,7 @@ export class TracingInterceptor { "overwrite": true } ###/TSAPI_WRITE_FILE###import { Metrics } from '@aws-lambda-powertools/metrics'; -import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const metrics = new Metrics(); @@ -191,15 +191,15 @@ export class MetricsInterceptor { "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###import { LoggingInterceptor } from './powertools/logger'; -import { MetricsInterceptor } from './powertools/metrics'; -import { TracingInterceptor } from './powertools/tracer'; -import { tryCatchInterceptor } from './try-catch'; - -export * from './try-catch'; -export * from './powertools/tracer'; -export * from './powertools/metrics'; -export * from './powertools/logger'; +###/TSAPI_WRITE_FILE###import { LoggingInterceptor } from './powertools/logger<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { MetricsInterceptor } from './powertools/metrics<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { TracingInterceptor } from './powertools/tracer<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { tryCatchInterceptor } from './try-catch<%_ if (metadata.esm) { _%>.js<%_ } _%>'; + +export * from './try-catch<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/tracer<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/metrics<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/logger<%_ if (metadata.esm) { _%>.js<%_ } _%>'; /** * All default interceptors, for logging, tracing, metrics, and error handling diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/operationConfig.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/operationConfig.ejs index 3acb4c029..279b24825 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/operationConfig.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/operationConfig.ejs @@ -13,12 +13,12 @@ import { <%- model.name %>FromJSON, <%- model.name %>ToJSON, <%_ }); _%> -} from '../models'; +} from '../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; <%_ const serviceClassName = services[0] ? services[0].className : "DefaultApi"; _%> // API Gateway Types import { APIGatewayProxyWebsocketEventV2, APIGatewayProxyResultV2, Context } from "aws-lambda"; -import { <%- serviceClassName %>ServerSdk } from "./server-sdk"; +import { <%- serviceClassName %>ServerSdk } from "./server-sdk<%_ if (metadata.esm) { _%>.js<%_ } _%>"; <%_ const toServerOperations = allOperations.filter(op => op.vendorExtensions && op.vendorExtensions['x-async'] && ['client_to_server', 'bidirectional'].includes(op.vendorExtensions['x-async'].direction)); _%> // Generic type for object keyed by operation names diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/serverSdk.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/serverSdk.ejs index 7647e8b8a..403ce25ef 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/serverSdk.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-async-runtime/templates/server/serverSdk.ejs @@ -13,7 +13,7 @@ import { <%- model.name %>FromJSON, <%- model.name %>ToJSON, <%_ }); _%> -} from "../models"; +} from "../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>"; import { ApiGatewayManagementApiClient, PostToConnectionCommand, diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/api.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/api.ejs index 40d9e1ce3..9ad8f4990 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/api.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/api.ejs @@ -9,7 +9,11 @@ ###/TSAPI_WRITE_FILE###import { TypeSafeRestApi, TypeSafeRestApiProps, TypeSafeApiIntegration } from "@aws/pdk/type-safe-api"; import { Construct } from "constructs"; import { OperationLookup, OperationConfig } from "<%- metadata.runtimePackageName %>"; +<%_ if (metadata.esm) { _%> +import * as url from 'url'; +<%_ } else { _%> import * as path from "path"; +<%_ } _%> export type ApiIntegrations = OperationConfig; @@ -26,7 +30,11 @@ export class Api extends TypeSafeRestApi { super(scope, id, { ...props, integrations: props.integrations as any, + <%_ if (metadata.esm) { _%> + specPath: url.fileURLToPath(new URL("<%- metadata.relativeSpecPath %>", import.meta.url)), + <%_ } else { _%> specPath: path.resolve(__dirname, "<%- metadata.relativeSpecPath %>"), + <%_ } _%> operationLookup: OperationLookup as any, }); } diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/functions.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/functions.ejs index dcf346291..64ae5fe87 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/functions.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/functions.ejs @@ -10,6 +10,9 @@ import { Duration } from "aws-cdk-lib"; import { SnapStartFunction, SnapStartFunctionProps } from "@aws/pdk/type-safe-api"; import { Code, Function, Runtime, Tracing, FunctionProps } from "aws-cdk-lib/aws-lambda"; import * as path from "path"; +<%_ if (metadata.esm) { _%> +import * as url from 'url'; +<%_ } _%> <%_ allOperations.forEach((operation) => { _%> <%_ if (operation.vendorExtensions && operation.vendorExtensions['x-handler']) { _%> @@ -42,7 +45,7 @@ export class <%- operation.operationIdPascalCase %>Function extends <% if (isJav <%_ } else if (isJava) { _%> handler: "<%- metadata['x-handlers-java-package'] %>.<%- operation.operationIdPascalCase %>Handler", <%_ } _%> - code: Code.fromAsset(path.resolve(__dirname, "..", + code: Code.fromAsset(<%_ if (metadata.esm) { _%>url.fileURLToPath(new URL(path.join("..",<%_ } else { _%>path.resolve(__dirname, "..",<%_ } %> <%_ if (isTypeScript) { _%> "<%- metadata['x-handlers-typescript-asset-path'] %>", "<%- operation.operationIdKebabCase %>", @@ -51,7 +54,7 @@ export class <%- operation.operationIdPascalCase %>Function extends <% if (isJav <%_ } else if (isJava) { _%> "<%- metadata['x-handlers-java-asset-path'] %>", <%_ } _%> - )), + )<%_ if (metadata.esm) { _%>, import.meta.url))<%_ } _%>), tracing: Tracing.ACTIVE, timeout: Duration.seconds(30), ...props, diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/index.ejs index 55d853ffa..682c30f3b 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/index.ejs @@ -6,6 +6,6 @@ "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###export * from "./api"; -export * from "./mock-integrations"; -export * from "./functions"; +###/TSAPI_WRITE_FILE###export * from "./api<%_ if (metadata.esm) { _%>.js<%_ } _%>"; +export * from "./mock-integrations<%_ if (metadata.esm) { _%>.js<%_ } _%>"; +export * from "./functions<%_ if (metadata.esm) { _%>.js<%_ } _%>"; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/mockIntegrations.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/mockIntegrations.ejs index 60a171999..244966399 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/mockIntegrations.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-cdk-infrastructure/templates/mockIntegrations.ejs @@ -14,6 +14,9 @@ import { Integrations, MockIntegration } from "@aws/pdk/type-safe-api"; import * as fs from "fs"; import * as path from "path"; +<%_ if (metadata.esm) { _%> +import * as url from 'url'; +<%_ } _%> /** * Type-safe mock integrations for API operations @@ -23,7 +26,15 @@ export class MockIntegrations { * Read a mock data file for the given operation */ private static readMockDataFile(method: string, urlPath: string, statusCode: number): string { - return fs.readFileSync(path.join(__dirname, "..", "mocks", `${method.toLowerCase()}${urlPath.replace(/\//g, "-")}-${statusCode}.json`), "utf-8"); + const mockPath = path.join("..", "mocks", `${method.toLowerCase()}${urlPath.replace(/\//g, "-")}-${statusCode}.json`); + return fs.readFileSync( + <%_ if (metadata.esm) { _%> + url.fileURLToPath(new URL(mockPath, import.meta.url)), + <%_ } else { _%> + path.join(__dirname, mockPath), + <%_ } _%> + "utf-8", + ); } <%_ if (metadata.enableMockIntegrations) { _%> diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-lambda-handlers/templates/tests.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-lambda-handlers/templates/tests.ejs index a2d54c9a7..1e837310e 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-lambda-handlers/templates/tests.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-lambda-handlers/templates/tests.ejs @@ -16,7 +16,7 @@ } from "<%= metadata.runtimePackageName %>"; import { <%= operation.name %> -} from "../<%= metadata.srcDir || 'src' %>/<%= operation.operationIdKebabCase %>"; +} from "../<%= metadata.srcDir || 'src' %>/<%= operation.operationIdKebabCase %><%_ if (metadata.esm) { _%>.js<%_ } _%>"; // Common request arguments const requestArguments = { @@ -25,7 +25,7 @@ const requestArguments = { context: {} as any, interceptorContext: { logger: { - info: jest.fn(), + info: <% if (metadata.vitest) { %>vi<% } else { %>jest<% } %>.fn(), }, }, } satisfies Omit<<%= operation.operationIdPascalCase %>ChainedRequestInput, 'input'>; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/client.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/client.ejs index 9c61e9244..53fae7419 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/client.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/client.ejs @@ -13,7 +13,7 @@ import { <%- model.name %>FromJSON, <%- model.name %>ToJSON, <%_ }); _%> -} from "../models"; +} from "../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>"; import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types'; import { HttpRequest } from "@aws-sdk/protocol-http"; import { SignatureV4 } from "@aws-sdk/signature-v4"; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/index.ejs index 1077cf604..cffe1a32a 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-websocket-client/templates/index.ejs @@ -8,5 +8,5 @@ } ###/TSAPI_WRITE_FILE###/* tslint:disable */ /* eslint-disable */ -export * from './models'; -export * from './client/client'; +export * from './models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; +export * from './client/client<%_ if (metadata.esm) { _%>.js<%_ } _%>'; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/apis.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/apis.ejs index dfe343fa5..b6b45276f 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/apis.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/apis.ejs @@ -20,19 +20,19 @@ * Do not edit the class manually. */ -import * as runtime from '../runtime'; +import * as runtime from '../runtime<%_ if (metadata.esm) { _%>.js<%_ } _%>'; <%_ if (service.modelImports.length > 0) { _%> import type { <%_ service.modelImports.forEach((modelImport) => { _%> <%- modelImport %>, <%_ }); _%> -} from '../models'; +} from '../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; import { <%_ service.modelImports.forEach((modelImport) => { _%> <%- modelImport %>FromJSON, <%- modelImport %>ToJSON, <%_ }); _%> -} from '../models'; +} from '../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; <%_ } _%> <%_ service.operations.forEach((operation) => { _%> diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/index.ejs index 26c88f45c..61d8edee9 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/index.ejs @@ -9,5 +9,5 @@ ###/TSAPI_WRITE_FILE###/* tslint:disable */ /* eslint-disable */ <%_ services.forEach((service) => { _%> -export * from './<%- service.name %>Api'; +export * from './<%- service.name %>Api<%_ if (metadata.esm) { _%>.js<%_ } _%>'; <%_ }); _%> diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/index.ejs index cbc388a17..2f7b7ab9f 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/index.ejs @@ -10,6 +10,6 @@ ###/TSAPI_WRITE_FILE###/* tslint:disable */ /* eslint-disable */ <%_ models.forEach((model) => { _%> -export * from './<%= model.name %>'; +export * from './<%= model.name %><%_ if (metadata.esm) { _%>.js<%_ } _%>'; <%_ }); _%> <%_ } _%> \ No newline at end of file diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/models.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/models.ejs index 669a66ddb..8f14af4fa 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/models.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/models.ejs @@ -19,7 +19,7 @@ * NOTE: This class is auto generated. * Do not edit the class manually. */ -import { exists, mapValues } from './model-utils'; +import { exists, mapValues } from './model-utils<%_ if (metadata.esm) { _%>.js<%_ } _%>'; <%_ const modelsByName = Object.fromEntries(models.map(m => [m.name, m])); _%> <%_ model.uniqueImports.forEach((importName) => { _%> import type { <%= importName %> } from './<%= importName %>'; @@ -30,7 +30,7 @@ import { <%_ if (!(modelsByName[importName] && modelsByName[importName].export === "enum")) { _%> instanceOf<%= importName %>, <%_ } _%> -} from './<%= importName %>'; +} from './<%= importName %><%_ if (metadata.esm) { _%>.js<%_ } _%>'; <%_ }); _%> <%_ const isComposite = model.export === "one-of" || model.export === "any-of" || model.export === "all-of"; _%> <%_ diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/index.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/index.ejs index e9ca8de3a..144849b0a 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/index.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/index.ejs @@ -8,9 +8,9 @@ } ###/TSAPI_WRITE_FILE###/* tslint:disable */ /* eslint-disable */ -export * from './runtime'; -export * from './apis'; -export * from './models'; -export * from './apis/DefaultApi/OperationConfig'; -export * from './response/response'; -export * from './interceptors' +export * from './runtime<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './apis<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; +export * from './models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; +export * from './apis/DefaultApi/OperationConfig<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './response/response<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './interceptors<%_ if (metadata.esm) { _%>/index.js<%_ } _%>' diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/interceptors.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/interceptors.ejs index a1da0d89c..50e3c0dc1 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/interceptors.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/interceptors.ejs @@ -8,7 +8,7 @@ ###/TSAPI_WRITE_FILE###import { ChainedRequestInput, OperationResponse, -} from '..'; +} from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; /** * Create an interceptor which returns the given error response and status should an error occur @@ -62,7 +62,7 @@ export const tryCatchInterceptor = buildTryCatchInterceptor(500, { message: 'Int "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###import { ChainedRequestInput, OperationResponse } from '..'; +###/TSAPI_WRITE_FILE###import { ChainedRequestInput, OperationResponse } from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; // By default, allow all origins and headers const DEFAULT_CORS_HEADERS: { [key: string]: string } = { @@ -111,7 +111,7 @@ export const corsInterceptor = buildResponseHeaderInterceptor(DEFAULT_CORS_HEADE "overwrite": true } ###/TSAPI_WRITE_FILE###import { Logger } from '@aws-lambda-powertools/logger'; -import { ChainedRequestInput, OperationResponse } from '../..'; +import { ChainedRequestInput, OperationResponse } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const logger = new Logger(); @@ -160,7 +160,7 @@ export class LoggingInterceptor { "overwrite": true } ###/TSAPI_WRITE_FILE###import { Tracer } from '@aws-lambda-powertools/tracer'; -import { ChainedRequestInput, OperationResponse } from '../..'; +import { ChainedRequestInput, OperationResponse } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const tracer = new Tracer(); @@ -251,7 +251,7 @@ export class TracingInterceptor { "overwrite": true } ###/TSAPI_WRITE_FILE###import { Metrics } from '@aws-lambda-powertools/metrics'; -import { ChainedRequestInput, OperationResponse } from '../..'; +import { ChainedRequestInput, OperationResponse } from '../..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; const metrics = new Metrics(); @@ -303,17 +303,17 @@ export class MetricsInterceptor { "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###import { corsInterceptor } from './cors'; -import { LoggingInterceptor } from './powertools/logger'; -import { MetricsInterceptor } from './powertools/metrics'; -import { TracingInterceptor } from './powertools/tracer'; -import { tryCatchInterceptor } from './try-catch'; +###/TSAPI_WRITE_FILE###import { corsInterceptor } from './cors<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { LoggingInterceptor } from './powertools/logger<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { MetricsInterceptor } from './powertools/metrics<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { TracingInterceptor } from './powertools/tracer<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +import { tryCatchInterceptor } from './try-catch<%_ if (metadata.esm) { _%>.js<%_ } _%>'; -export * from './cors'; -export * from './try-catch'; -export * from './powertools/tracer'; -export * from './powertools/metrics'; -export * from './powertools/logger'; +export * from './cors<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './try-catch<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/tracer<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/metrics<%_ if (metadata.esm) { _%>.js<%_ } _%>'; +export * from './powertools/logger<%_ if (metadata.esm) { _%>.js<%_ } _%>'; /** * All default interceptors, for logging, tracing, metrics, cors headers and error handling diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/operationConfig.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/operationConfig.ejs index a8ec77422..4bec9de3c 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/operationConfig.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/operationConfig.ejs @@ -13,7 +13,7 @@ import { <%- model.name %>FromJSON, <%- model.name %>ToJSON, <%_ }); _%> -} from '../../models'; +} from '../../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; // Import request parameter interfaces import { <%_ allOperations.forEach((operation) => { _%> @@ -21,7 +21,7 @@ import { <%- operation.operationIdPascalCase %>Request, <%_ } _%> <%_ }); _%> -} from '..'; +} from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; // API Gateway Types import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda"; diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/response.ejs b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/response.ejs index 184cc9305..d25dbee80 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/response.ejs +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/response.ejs @@ -6,7 +6,7 @@ "ext": ".ts", "overwrite": true } -###/TSAPI_WRITE_FILE###import { OperationResponse } from '..'; +###/TSAPI_WRITE_FILE###import { OperationResponse } from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>'; /** diff --git a/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-cdk-infrastructure.test.ts.snap b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-cdk-infrastructure.test.ts.snap index f9917788c..1b8eb97cd 100644 --- a/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-cdk-infrastructure.test.ts.snap +++ b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-cdk-infrastructure.test.ts.snap @@ -257,7 +257,11 @@ export class MockIntegrations { * Read a mock data file for the given operation */ private static readMockDataFile(method: string, urlPath: string, statusCode: number): string { - return fs.readFileSync(path.join(__dirname, "..", "mocks", \`\${method.toLowerCase()}\${urlPath.replace(/\\//g, "-")}-\${statusCode}.json\`), "utf-8"); + const mockPath = path.join("..", "mocks", \`\${method.toLowerCase()}\${urlPath.replace(/\\//g, "-")}-\${statusCode}.json\`); + return fs.readFileSync( + path.join(__dirname, mockPath), + "utf-8", + ); } // No mock integrations have been generated, since mock data generation is disabled. @@ -322,7 +326,11 @@ export class MockIntegrations { * Read a mock data file for the given operation */ private static readMockDataFile(method: string, urlPath: string, statusCode: number): string { - return fs.readFileSync(path.join(__dirname, "..", "mocks", \`\${method.toLowerCase()}\${urlPath.replace(/\\//g, "-")}-\${statusCode}.json\`), "utf-8"); + const mockPath = path.join("..", "mocks", \`\${method.toLowerCase()}\${urlPath.replace(/\\//g, "-")}-\${statusCode}.json\`); + return fs.readFileSync( + path.join(__dirname, mockPath), + "utf-8", + ); } /** diff --git a/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-esm.test.ts.snap b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-esm.test.ts.snap new file mode 100644 index 000000000..073a13a4c --- /dev/null +++ b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript-esm.test.ts.snap @@ -0,0 +1,4620 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TypeScript ESM Generator Tests Generates typescript with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "src/apis/DefaultApi.ts +src/apis/index.ts +src/models/index.ts +src/models/model-utils.ts +src/models/BadRequestErrorResponseContent.ts +src/models/InternalFailureErrorResponseContent.ts +src/models/NotAuthorizedErrorResponseContent.ts +src/models/SuccessResponseContent.ts +src/runtime.ts +src/index.ts +src/interceptors/try-catch.ts +src/interceptors/cors.ts +src/interceptors/powertools/logger.ts +src/interceptors/powertools/tracer.ts +src/interceptors/powertools/metrics.ts +src/interceptors/index.ts +src/apis/DefaultApi/OperationConfig.ts +src/response/response.ts", + "src/apis/DefaultApi.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ + +import * as runtime from '../runtime.js'; +import type { + BadRequestErrorResponseContent, + InternalFailureErrorResponseContent, + NotAuthorizedErrorResponseContent, + SuccessResponseContent, +} from '../models/index.js'; +import { + BadRequestErrorResponseContentFromJSON, + BadRequestErrorResponseContentToJSON, + InternalFailureErrorResponseContentFromJSON, + InternalFailureErrorResponseContentToJSON, + NotAuthorizedErrorResponseContentFromJSON, + NotAuthorizedErrorResponseContentToJSON, + SuccessResponseContentFromJSON, + SuccessResponseContentToJSON, +} from '../models/index.js'; + +export interface JavaOneRequest { + name: string; +} + +export interface JavaTwoRequest { + name: string; +} + +export interface PythonOneRequest { + name: string; +} + +export interface PythonTwoRequest { + name: string; +} + +export interface TypescriptOneRequest { + name: string; +} + +export interface TypescriptTwoRequest { + name: string; +} + +/** + * + */ +export class DefaultApi extends runtime.BaseAPI { + /** + * + */ + async javaOneRaw(requestParameters: JavaOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling javaOne.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/java/1\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async javaOne(requestParameters: JavaOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.javaOneRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * + */ + async javaTwoRaw(requestParameters: JavaTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling javaTwo.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/java/2\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async javaTwo(requestParameters: JavaTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.javaTwoRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * + */ + async pythonOneRaw(requestParameters: PythonOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling pythonOne.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/python/1\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async pythonOne(requestParameters: PythonOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.pythonOneRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * + */ + async pythonTwoRaw(requestParameters: PythonTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling pythonTwo.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/python/2\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async pythonTwo(requestParameters: PythonTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.pythonTwoRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * + */ + async typescriptOneRaw(requestParameters: TypescriptOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling typescriptOne.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/typescript/1\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async typescriptOne(requestParameters: TypescriptOneRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.typescriptOneRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * + */ + async typescriptTwoRaw(requestParameters: TypescriptTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.name === null || requestParameters.name === undefined) { + throw new runtime.RequiredError('name','Required parameter requestParameters.name was null or undefined when calling typescriptTwo.'); + } + + const queryParameters: any = {}; + + if (requestParameters.name !== undefined) { + queryParameters['name'] = requestParameters.name; + } + + + const headerParameters: runtime.HTTPHeaders = {}; + + + + const response = await this.request({ + path: \`/typescript/2\`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseContentFromJSON(jsonValue)); + } + + /** + * + */ + async typescriptTwo(requestParameters: TypescriptTwoRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.typescriptTwoRaw(requestParameters, initOverrides); + return await response.value(); + } + +} + +", + "src/apis/DefaultApi/OperationConfig.ts": "// Import models +import { + BadRequestErrorResponseContent, + BadRequestErrorResponseContentFromJSON, + BadRequestErrorResponseContentToJSON, + InternalFailureErrorResponseContent, + InternalFailureErrorResponseContentFromJSON, + InternalFailureErrorResponseContentToJSON, + NotAuthorizedErrorResponseContent, + NotAuthorizedErrorResponseContentFromJSON, + NotAuthorizedErrorResponseContentToJSON, + SuccessResponseContent, + SuccessResponseContentFromJSON, + SuccessResponseContentToJSON, +} from '../../models/index.js'; +// Import request parameter interfaces +import { + JavaOneRequest, + JavaTwoRequest, + PythonOneRequest, + PythonTwoRequest, + TypescriptOneRequest, + TypescriptTwoRequest, +} from '../index.js'; + +// API Gateway Types +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda"; + +// Generic type for object keyed by operation names +export interface OperationConfig { + javaOne: T; + javaTwo: T; + pythonOne: T; + pythonTwo: T; + typescriptOne: T; + typescriptTwo: T; +} + +// Look up path and http method for a given operation name +export const OperationLookup = { + javaOne: { + path: '/java/1', + method: 'GET', + contentTypes: ['application/json'], + }, + javaTwo: { + path: '/java/2', + method: 'GET', + contentTypes: ['application/json'], + }, + pythonOne: { + path: '/python/1', + method: 'GET', + contentTypes: ['application/json'], + }, + pythonTwo: { + path: '/python/2', + method: 'GET', + contentTypes: ['application/json'], + }, + typescriptOne: { + path: '/typescript/1', + method: 'GET', + contentTypes: ['application/json'], + }, + typescriptTwo: { + path: '/typescript/2', + method: 'GET', + contentTypes: ['application/json'], + }, +}; + +export class Operations { + /** + * Return an OperationConfig with the same value for every operation + */ + public static all = (value: T): OperationConfig => Object.fromEntries( + Object.keys(OperationLookup).map((operationId) => [operationId, value]) + ) as unknown as OperationConfig; +} + +// Standard apigateway request parameters (query parameters or path parameters, multi or single value) +type ApiGatewayRequestParameters = { [key: string]: string | string[] | undefined }; + +/** + * URI decode for a string or array of strings + */ +const uriDecode = (value: string | string[]): string | string[] => + typeof value === 'string' ? decodeURIComponent(value) : value.map((v) => decodeURIComponent(v)); + +/** + * URI decodes apigateway request parameters (query or path parameters) + */ +const decodeRequestParameters = (parameters: ApiGatewayRequestParameters): ApiGatewayRequestParameters => { + const decodedParameters = {}; + Object.keys(parameters || {}).forEach((key) => { + decodedParameters[key] = parameters[key] ? uriDecode(parameters[key]) : parameters[key]; + }); + return decodedParameters; +}; + +/** + * Parse the body if the content type is json, otherwise leave as a raw string + */ +const parseBody = (body: string, demarshal: (body: string) => any, contentTypes: string[]): any => contentTypes.filter((contentType) => contentType !== 'application/json').length === 0 ? demarshal(body || '{}') : body; + +const assertRequired = (required: boolean, baseName: string, parameters: any) => { + if(required && parameters[baseName] === undefined) { + throw new Error(\`Missing required request parameter '\${baseName}'\`); + } +}; + +const coerceNumber = (baseName: string, s: string, isInteger: boolean): number => { + const n = Number(s); + if (isNaN(n)) { + throw new Error(\`Expected a number for request parameter '\${baseName}'\`); + } + if (isInteger && !Number.isInteger(n)) { + throw new Error(\`Expected an integer for request parameter '\${baseName}'\`); + } + return n; +}; + +const coerceBoolean = (baseName: string, s: string): boolean => { + switch (s) { + case "true": + return true; + case "false": + return false; + default: + throw new Error(\`Expected a boolean (true or false) for request parameter '\${baseName}'\`); + } +}; + +const coerceDate = (baseName: string, s: string): Date => { + const d = new Date(s); + if (isNaN(d as any)) { + throw new Error(\`Expected a valid date (iso format) for request parameter '\${baseName}'\`); + } + return d; +}; + +const coerceParameter = ( + baseName: string, + dataType: string, + isInteger: boolean, + rawStringParameters: { [key: string]: string | undefined }, + rawStringArrayParameters: { [key: string]: string[] | undefined }, + required: boolean, +) => { + switch (dataType) { + case "number": + assertRequired(required, baseName, rawStringParameters); + return rawStringParameters[baseName] !== undefined ? coerceNumber(baseName, rawStringParameters[baseName], isInteger) : undefined; + case "boolean": + assertRequired(required, baseName, rawStringParameters); + return rawStringParameters[baseName] !== undefined ? coerceBoolean(baseName, rawStringParameters[baseName]) : undefined; + case "Date": + assertRequired(required, baseName, rawStringParameters); + return rawStringParameters[baseName] !== undefined ? coerceDate(baseName, rawStringParameters[baseName]) : undefined; + case "Array": + assertRequired(required, baseName, rawStringArrayParameters); + return rawStringArrayParameters[baseName] !== undefined ? rawStringArrayParameters[baseName].map(n => coerceNumber(baseName, n, isInteger)) : undefined; + case "Array": + assertRequired(required, baseName, rawStringArrayParameters); + return rawStringArrayParameters[baseName] !== undefined ? rawStringArrayParameters[baseName].map(n => coerceBoolean(baseName, n)) : undefined; + case "Array": + assertRequired(required, baseName, rawStringArrayParameters); + return rawStringArrayParameters[baseName] !== undefined ? rawStringArrayParameters[baseName].map(n => coerceDate(baseName, n)) : undefined; + case "Array": + assertRequired(required, baseName, rawStringArrayParameters); + return rawStringArrayParameters[baseName]; + case "string": + default: + assertRequired(required, baseName, rawStringParameters); + return rawStringParameters[baseName]; + } +}; + +const extractResponseHeadersFromInterceptors = (interceptors: any[]): { [key: string]: string } => { + return (interceptors ?? []).reduce((interceptor: any, headers: { [key: string]: string }) => ({ + ...headers, + ...(interceptor?.__type_safe_api_response_headers ?? {}), + }), {} as { [key: string]: string }); +}; + +export type OperationIds = | 'javaOne' | 'javaTwo' | 'pythonOne' | 'pythonTwo' | 'typescriptOne' | 'typescriptTwo'; +export type OperationApiGatewayProxyResult = APIGatewayProxyResult & { __operationId?: T }; + +// Api gateway lambda handler type +export type OperationApiGatewayLambdaHandler = (event: APIGatewayProxyEvent, context: Context) => Promise>; + +// Type of the response to be returned by an operation lambda handler +export interface OperationResponse { + statusCode: StatusCode; + headers?: { [key: string]: string }; + multiValueHeaders?: { [key: string]: string[] }; + body: Body; +} + +// Input for a lambda handler for an operation +export type LambdaRequestParameters = { + requestParameters: RequestParameters, + body: RequestBody, +}; + +export type InterceptorContext = { [key: string]: any }; + +export interface RequestInput { + input: LambdaRequestParameters; + event: APIGatewayProxyEvent; + context: Context; + interceptorContext: InterceptorContext; +} + +export interface ChainedRequestInput extends RequestInput { + chain: LambdaHandlerChain; +} + +/** + * A lambda handler function which is part of a chain. It may invoke the remainder of the chain via the given chain input + */ +export type ChainedLambdaHandlerFunction = ( + input: ChainedRequestInput, +) => Promise; + +// Type for a lambda handler function to be wrapped +export type LambdaHandlerFunction = ( + input: RequestInput, +) => Promise; + +export interface LambdaHandlerChain { + next: LambdaHandlerFunction; +} + +// Interceptor is a type alias for ChainedLambdaHandlerFunction +export type Interceptor = ChainedLambdaHandlerFunction; + +/** + * Build a chain from the given array of chained lambda handlers + */ +const buildHandlerChain = ( + ...handlers: ChainedLambdaHandlerFunction[] +): LambdaHandlerChain => { + if (handlers.length === 0) { + return { + next: () => { + throw new Error("No more handlers remain in the chain! The last handler should not call next."); + } + }; + } + const [currentHandler, ...remainingHandlers] = handlers; + return { + next: (input) => { + return currentHandler({ + ...input, + chain: buildHandlerChain(...remainingHandlers), + }); + }, + }; +}; + +/** + * Path, Query and Header parameters for JavaOne + */ +export interface JavaOneRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for JavaOne + */ +export type JavaOneRequestBody = never; + +export type JavaOne200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type JavaOne400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type JavaOne403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type JavaOne500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type JavaOneOperationResponses = | JavaOne200OperationResponse | JavaOne400OperationResponse | JavaOne403OperationResponse | JavaOne500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type JavaOneHandlerFunction = LambdaHandlerFunction; +export type JavaOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type JavaOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of javaOne + */ +export const javaOneHandler = ( + ...handlers: [JavaOneChainedHandlerFunction, ...JavaOneChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'javaOne'> => async (event: any, context: any, _callback?: any, additionalInterceptors: JavaOneChainedHandlerFunction[] = []): Promise => { + const operationId = "javaOne"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: JavaOneRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as JavaOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; +/** + * Path, Query and Header parameters for JavaTwo + */ +export interface JavaTwoRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for JavaTwo + */ +export type JavaTwoRequestBody = never; + +export type JavaTwo200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type JavaTwo400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type JavaTwo403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type JavaTwo500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type JavaTwoOperationResponses = | JavaTwo200OperationResponse | JavaTwo400OperationResponse | JavaTwo403OperationResponse | JavaTwo500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type JavaTwoHandlerFunction = LambdaHandlerFunction; +export type JavaTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type JavaTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of javaTwo + */ +export const javaTwoHandler = ( + ...handlers: [JavaTwoChainedHandlerFunction, ...JavaTwoChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'javaTwo'> => async (event: any, context: any, _callback?: any, additionalInterceptors: JavaTwoChainedHandlerFunction[] = []): Promise => { + const operationId = "javaTwo"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: JavaTwoRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as JavaTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; +/** + * Path, Query and Header parameters for PythonOne + */ +export interface PythonOneRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for PythonOne + */ +export type PythonOneRequestBody = never; + +export type PythonOne200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type PythonOne400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type PythonOne403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type PythonOne500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type PythonOneOperationResponses = | PythonOne200OperationResponse | PythonOne400OperationResponse | PythonOne403OperationResponse | PythonOne500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type PythonOneHandlerFunction = LambdaHandlerFunction; +export type PythonOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type PythonOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of pythonOne + */ +export const pythonOneHandler = ( + ...handlers: [PythonOneChainedHandlerFunction, ...PythonOneChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'pythonOne'> => async (event: any, context: any, _callback?: any, additionalInterceptors: PythonOneChainedHandlerFunction[] = []): Promise => { + const operationId = "pythonOne"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: PythonOneRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as PythonOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; +/** + * Path, Query and Header parameters for PythonTwo + */ +export interface PythonTwoRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for PythonTwo + */ +export type PythonTwoRequestBody = never; + +export type PythonTwo200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type PythonTwo400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type PythonTwo403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type PythonTwo500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type PythonTwoOperationResponses = | PythonTwo200OperationResponse | PythonTwo400OperationResponse | PythonTwo403OperationResponse | PythonTwo500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type PythonTwoHandlerFunction = LambdaHandlerFunction; +export type PythonTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type PythonTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of pythonTwo + */ +export const pythonTwoHandler = ( + ...handlers: [PythonTwoChainedHandlerFunction, ...PythonTwoChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'pythonTwo'> => async (event: any, context: any, _callback?: any, additionalInterceptors: PythonTwoChainedHandlerFunction[] = []): Promise => { + const operationId = "pythonTwo"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: PythonTwoRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as PythonTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; +/** + * Path, Query and Header parameters for TypescriptOne + */ +export interface TypescriptOneRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for TypescriptOne + */ +export type TypescriptOneRequestBody = never; + +export type TypescriptOne200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type TypescriptOne400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type TypescriptOne403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type TypescriptOne500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type TypescriptOneOperationResponses = | TypescriptOne200OperationResponse | TypescriptOne400OperationResponse | TypescriptOne403OperationResponse | TypescriptOne500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type TypescriptOneHandlerFunction = LambdaHandlerFunction; +export type TypescriptOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type TypescriptOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of typescriptOne + */ +export const typescriptOneHandler = ( + ...handlers: [TypescriptOneChainedHandlerFunction, ...TypescriptOneChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'typescriptOne'> => async (event: any, context: any, _callback?: any, additionalInterceptors: TypescriptOneChainedHandlerFunction[] = []): Promise => { + const operationId = "typescriptOne"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: TypescriptOneRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as TypescriptOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; +/** + * Path, Query and Header parameters for TypescriptTwo + */ +export interface TypescriptTwoRequestParameters { + readonly name: string; +} + +/** + * Request body parameter for TypescriptTwo + */ +export type TypescriptTwoRequestBody = never; + +export type TypescriptTwo200OperationResponse = OperationResponse<200, SuccessResponseContent>; +export type TypescriptTwo400OperationResponse = OperationResponse<400, BadRequestErrorResponseContent>; +export type TypescriptTwo403OperationResponse = OperationResponse<403, NotAuthorizedErrorResponseContent>; +export type TypescriptTwo500OperationResponse = OperationResponse<500, InternalFailureErrorResponseContent>; + +export type TypescriptTwoOperationResponses = | TypescriptTwo200OperationResponse | TypescriptTwo400OperationResponse | TypescriptTwo403OperationResponse | TypescriptTwo500OperationResponse ; + +// Type that the handler function provided to the wrapper must conform to +export type TypescriptTwoHandlerFunction = LambdaHandlerFunction; +export type TypescriptTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type TypescriptTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of typescriptTwo + */ +export const typescriptTwoHandler = ( + ...handlers: [TypescriptTwoChainedHandlerFunction, ...TypescriptTwoChainedHandlerFunction[]] +): OperationApiGatewayLambdaHandler<'typescriptTwo'> => async (event: any, context: any, _callback?: any, additionalInterceptors: TypescriptTwoChainedHandlerFunction[] = []): Promise => { + const operationId = "typescriptTwo"; + + const rawSingleValueParameters = decodeRequestParameters({ + ...(event.pathParameters || {}), + ...(event.queryStringParameters || {}), + ...(event.headers || {}), + }) as { [key: string]: string | undefined }; + const rawMultiValueParameters = decodeRequestParameters({ + ...(event.multiValueQueryStringParameters || {}), + ...(event.multiValueHeaders || {}), + }) as { [key: string]: string[] | undefined }; + + const marshal = (statusCode: number, responseBody: any): string => { + let marshalledBody = responseBody; + switch(statusCode) { + case 200: + marshalledBody = JSON.stringify(SuccessResponseContentToJSON(marshalledBody)); + break; + case 400: + marshalledBody = JSON.stringify(BadRequestErrorResponseContentToJSON(marshalledBody)); + break; + case 403: + marshalledBody = JSON.stringify(NotAuthorizedErrorResponseContentToJSON(marshalledBody)); + break; + case 500: + marshalledBody = JSON.stringify(InternalFailureErrorResponseContentToJSON(marshalledBody)); + break; + default: + break; + } + + return marshalledBody; + }; + + const errorHeaders = (statusCode: number): { [key: string]: string } => { + let headers = {}; + + switch(statusCode) { + case 400: { + if ("BadRequestErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "BadRequestErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 403: { + if ("NotAuthorizedErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "NotAuthorizedErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + case 500: { + if ("InternalFailureErrorResponseContent".endsWith("ResponseContent")) { + headers["x-amzn-errortype"] = "InternalFailureErrorResponseContent".slice(0, -"ResponseContent".length); + } + break; + } + default: + break; + } + + return headers; + }; + + let requestParameters: TypescriptTwoRequestParameters | undefined = undefined; + + try { + requestParameters = { + name: coerceParameter("name", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, + headers: { + ...errorHeaders(res.statusCode), + ...res.headers, + }, + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + + const demarshal = (bodyString: string): any => { + return {}; + }; + const body = parseBody(event.body, demarshal, ['application/json']) as TypescriptTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + + return { + ...response, + headers: { + ...errorHeaders(response.statusCode), + ...response.headers, + }, + body: response.body ? marshal(response.statusCode, response.body) : '', + }; +}; + +export interface HandlerRouterHandlers { + readonly javaOne: OperationApiGatewayLambdaHandler<'javaOne'>; + readonly javaTwo: OperationApiGatewayLambdaHandler<'javaTwo'>; + readonly pythonOne: OperationApiGatewayLambdaHandler<'pythonOne'>; + readonly pythonTwo: OperationApiGatewayLambdaHandler<'pythonTwo'>; + readonly typescriptOne: OperationApiGatewayLambdaHandler<'typescriptOne'>; + readonly typescriptTwo: OperationApiGatewayLambdaHandler<'typescriptTwo'>; +} + +export type AnyOperationRequestParameters = | JavaOneRequestParameters| JavaTwoRequestParameters| PythonOneRequestParameters| PythonTwoRequestParameters| TypescriptOneRequestParameters| TypescriptTwoRequestParameters; +export type AnyOperationRequestBodies = | JavaOneRequestBody| JavaTwoRequestBody| PythonOneRequestBody| PythonTwoRequestBody| TypescriptOneRequestBody| TypescriptTwoRequestBody; +export type AnyOperationResponses = | JavaOneOperationResponses| JavaTwoOperationResponses| PythonOneOperationResponses| PythonTwoOperationResponses| TypescriptOneOperationResponses| TypescriptTwoOperationResponses; + +export interface HandlerRouterProps< + RequestParameters, + RequestBody, + Response extends AnyOperationResponses +> { + /** + * Interceptors to apply to all handlers + */ + readonly interceptors?: ChainedLambdaHandlerFunction< + RequestParameters, + RequestBody, + Response + >[]; + + /** + * Handlers to register for each operation + */ + readonly handlers: HandlerRouterHandlers; +} + +const concatMethodAndPath = (method: string, path: string) => \`\${method.toLowerCase()}||\${path}\`; + +const OperationIdByMethodAndPath = Object.fromEntries(Object.entries(OperationLookup).map( + ([operationId, methodAndPath]) => [concatMethodAndPath(methodAndPath.method, methodAndPath.path), operationId] +)); + +/** + * Returns a lambda handler which can be used to route requests to the appropriate typed lambda handler function. + */ +export const handlerRouter = (props: HandlerRouterProps< + AnyOperationRequestParameters, + AnyOperationRequestBodies, + AnyOperationResponses +>): OperationApiGatewayLambdaHandler => async (event, context) => { + const operationId = OperationIdByMethodAndPath[concatMethodAndPath(event.requestContext.httpMethod, event.requestContext.resourcePath)]; + const handler = props.handlers[operationId]; + return handler(event, context, undefined, props.interceptors); +}; +", + "src/apis/index.ts": "/* tslint:disable */ +/* eslint-disable */ +export * from './DefaultApi.js'; +", + "src/index.ts": "/* tslint:disable */ +/* eslint-disable */ +export * from './runtime.js'; +export * from './apis/index.js'; +export * from './models/index.js'; +export * from './apis/DefaultApi/OperationConfig.js'; +export * from './response/response.js'; +export * from './interceptors/index.js' +", + "src/interceptors/cors.ts": "import { ChainedRequestInput, OperationResponse } from '../index.js'; + +// By default, allow all origins and headers +const DEFAULT_CORS_HEADERS: { [key: string]: string } = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', +}; + +/** + * Create an interceptor for adding headers to the response + * @param additionalHeaders headers to add to the response + */ +export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => { + const interceptor = async < + RequestParameters, + RequestBody, + Response extends OperationResponse + >( + request: ChainedRequestInput, + ): Promise => { + const result = await request.chain.next(request); + return { + ...result, + headers: { + ...additionalHeaders, + ...result.headers, + }, + }; + }; + + // Any error responses returned during request validation will include the headers + (interceptor as any).__type_safe_api_response_headers = additionalHeaders; + + return interceptor; +}; + +/** + * An interceptor for adding cross-origin resource sharing (CORS) headers to the response. + * Allows all origins and headers. Use buildResponseHeaderInterceptor to customise. + */ +export const corsInterceptor = buildResponseHeaderInterceptor(DEFAULT_CORS_HEADERS); +", + "src/interceptors/index.ts": "import { corsInterceptor } from './cors.js'; +import { LoggingInterceptor } from './powertools/logger.js'; +import { MetricsInterceptor } from './powertools/metrics.js'; +import { TracingInterceptor } from './powertools/tracer.js'; +import { tryCatchInterceptor } from './try-catch.js'; + +export * from './cors.js'; +export * from './try-catch.js'; +export * from './powertools/tracer.js'; +export * from './powertools/metrics.js'; +export * from './powertools/logger.js'; + +/** + * All default interceptors, for logging, tracing, metrics, cors headers and error handling + */ +export const INTERCEPTORS = [ + corsInterceptor, + LoggingInterceptor.intercept, + tryCatchInterceptor, + TracingInterceptor.intercept, + MetricsInterceptor.intercept, +] as const; +", + "src/interceptors/powertools/logger.ts": "import { Logger } from '@aws-lambda-powertools/logger'; +import { ChainedRequestInput, OperationResponse } from '../../index.js'; + +const logger = new Logger(); + +export class LoggingInterceptor { + /** + * Interceptor which adds an aws lambda powertools logger to the interceptor context, + * and adds the lambda context + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/ + */ + public static intercept = async < + RequestParameters, + RequestArrayParameters, + RequestBody, + Response extends OperationResponse + >( + request: ChainedRequestInput, + ): Promise => { + logger.addContext(request.context); + logger.appendKeys({ operationId: request.interceptorContext.operationId }); + request.interceptorContext.logger = logger; + const response = await request.chain.next(request); + logger.removeKeys(['operationId']); + return response; + }; + + /** + * Retrieve the logger from the interceptor context + */ + public static getLogger = < + RequestParameters, + RequestArrayParameters, + RequestBody, + Response extends OperationResponse + >(request: ChainedRequestInput): Logger => { + if (!request.interceptorContext.logger) { + throw new Error('No logger found, did you configure the LoggingInterceptor?'); + } + return request.interceptorContext.logger; + }; +} +", + "src/interceptors/powertools/metrics.ts": "import { Metrics } from '@aws-lambda-powertools/metrics'; +import { ChainedRequestInput, OperationResponse } from '../../index.js'; + +const metrics = new Metrics(); + +export class MetricsInterceptor { + /** + * Interceptor which adds an instance of aws lambda powertools metrics to the interceptor context, + * and ensures metrics are flushed prior to finishing the lambda execution + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/ + */ + public static intercept = async < + RequestParameters, + RequestArrayParameters, + RequestBody, + Response extends OperationResponse + >( + request: ChainedRequestInput, + ): Promise => { + metrics.addDimension("operationId", request.interceptorContext.operationId); + request.interceptorContext.metrics = metrics; + try { + return await request.chain.next(request); + } finally { + // Flush metrics + metrics.publishStoredMetrics(); + } + }; + + /** + * Retrieve the metrics logger from the request + */ + public static getMetrics = < + RequestParameters, + RequestArrayParameters, + RequestBody, + Response extends OperationResponse + >( + request: ChainedRequestInput, + ): Metrics => { + if (!request.interceptorContext.metrics) { + throw new Error('No metrics logger found, did you configure the MetricsInterceptor?'); + } + return request.interceptorContext.metrics; + }; +} +", + "src/interceptors/powertools/tracer.ts": "import { Tracer } from '@aws-lambda-powertools/tracer'; +import { ChainedRequestInput, OperationResponse } from '../../index.js'; + +const tracer = new Tracer(); + +export interface TracingInterceptorOptions { + /** + * Whether to add the response as metadata to the trace + */ + readonly addResponseAsMetadata?: boolean; +} + +/** + * Create an interceptor which adds an aws lambda powertools tracer to the interceptor context, + * creating the appropriate segment for the handler execution and annotating with recommended + * details. + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/tracer/#lambda-handler + */ +export const buildTracingInterceptor = (options?: TracingInterceptorOptions) => async < + RequestParameters, + RequestBody, + Response extends OperationResponse +>( + request: ChainedRequestInput, +): Promise => { + const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler'; + const segment = tracer.getSegment(); + let subsegment; + if (segment) { + subsegment = segment.addNewSubsegment(handler); + tracer.setSegment(subsegment); + } + + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + if (request.interceptorContext.logger) { + tracer.provider.setLogger(request.interceptorContext.logger); + } + + request.interceptorContext.tracer = tracer; + + try { + const result = await request.chain.next(request); + if (options?.addResponseAsMetadata) { + tracer.addResponseAsMetadata(result, handler); + } + return result; + } catch (e) { + tracer.addErrorAsMetadata(e as Error); + throw e; + } finally { + if (segment && subsegment) { + subsegment.close(); + tracer.setSegment(segment); + } + } +}; + +export class TracingInterceptor { + /** + * Interceptor which adds an aws lambda powertools tracer to the interceptor context, + * creating the appropriate segment for the handler execution and annotating with recommended + * details. + */ + public static intercept = buildTracingInterceptor(); + + /** + * Get the tracer from the interceptor context + */ + public static getTracer = < + RequestParameters, + RequestArrayParameters, + RequestBody, + Response extends OperationResponse + >( + request: ChainedRequestInput, + ): Tracer => { + if (!request.interceptorContext.tracer) { + throw new Error('No tracer found, did you configure the TracingInterceptor?'); + } + return request.interceptorContext.tracer; + }; +} +", + "src/interceptors/try-catch.ts": "import { + ChainedRequestInput, + OperationResponse, +} from '../index.js'; + +/** + * Create an interceptor which returns the given error response and status should an error occur + * @param statusCode the status code to return when an error is thrown + * @param errorResponseBody the body to return when an error occurs + */ +export const buildTryCatchInterceptor = ( + statusCode: TStatus, + errorResponseBody: ErrorResponseBody, +) => async < + RequestParameters, + RequestBody, + Response extends OperationResponse, +>( + request: ChainedRequestInput< + RequestParameters, + RequestBody, + Response + >, +): Promise> => { + try { + return await request.chain.next(request); + } catch (e: any) { + // If the error looks like a response, return it as the response + if ('statusCode' in e) { + return e; + } + + // Log the error if the logger is present + if (request.interceptorContext.logger && request.interceptorContext.logger.error) { + request.interceptorContext.logger.error('Interceptor caught exception', e as Error); + } else { + console.error('Interceptor caught exception', e); + } + + // Return the default error message + return { statusCode, body: errorResponseBody }; + } +}; + +/** + * Interceptor for catching unhandled exceptions and returning a 500 error. + * Uncaught exceptions which look like OperationResponses will be returned, such that deeply nested code may return error + * responses, eg: \`throw ApiResponse.notFound({ message: 'Not found!' })\` + */ +export const tryCatchInterceptor = buildTryCatchInterceptor(500, { message: 'Internal Error' }); +", + "src/models/BadRequestErrorResponseContent.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ +import { exists, mapValues } from './model-utils.js'; + +/** + * + * @export + * @interface BadRequestErrorResponseContent + */ +export interface BadRequestErrorResponseContent { + /** + * + * @type {string} + * @memberof BadRequestErrorResponseContent + */ + message: string; +} + + +/** + * Check if a given object implements the BadRequestErrorResponseContent interface. + */ +export function instanceOfBadRequestErrorResponseContent(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "message" in value; + return isInstance; +} + +export function BadRequestErrorResponseContentFromJSON(json: any): BadRequestErrorResponseContent { + return BadRequestErrorResponseContentFromJSONTyped(json, false); +} + +export function BadRequestErrorResponseContentFromJSONTyped(json: any, ignoreDiscriminator: boolean): BadRequestErrorResponseContent { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'message': json['message'], + }; +} + +export function BadRequestErrorResponseContentToJSON(value?: BadRequestErrorResponseContent | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'message': value.message, + }; +} + +", + "src/models/InternalFailureErrorResponseContent.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ +import { exists, mapValues } from './model-utils.js'; + +/** + * + * @export + * @interface InternalFailureErrorResponseContent + */ +export interface InternalFailureErrorResponseContent { + /** + * + * @type {string} + * @memberof InternalFailureErrorResponseContent + */ + message: string; +} + + +/** + * Check if a given object implements the InternalFailureErrorResponseContent interface. + */ +export function instanceOfInternalFailureErrorResponseContent(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "message" in value; + return isInstance; +} + +export function InternalFailureErrorResponseContentFromJSON(json: any): InternalFailureErrorResponseContent { + return InternalFailureErrorResponseContentFromJSONTyped(json, false); +} + +export function InternalFailureErrorResponseContentFromJSONTyped(json: any, ignoreDiscriminator: boolean): InternalFailureErrorResponseContent { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'message': json['message'], + }; +} + +export function InternalFailureErrorResponseContentToJSON(value?: InternalFailureErrorResponseContent | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'message': value.message, + }; +} + +", + "src/models/NotAuthorizedErrorResponseContent.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ +import { exists, mapValues } from './model-utils.js'; + +/** + * + * @export + * @interface NotAuthorizedErrorResponseContent + */ +export interface NotAuthorizedErrorResponseContent { + /** + * + * @type {string} + * @memberof NotAuthorizedErrorResponseContent + */ + message: string; +} + + +/** + * Check if a given object implements the NotAuthorizedErrorResponseContent interface. + */ +export function instanceOfNotAuthorizedErrorResponseContent(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "message" in value; + return isInstance; +} + +export function NotAuthorizedErrorResponseContentFromJSON(json: any): NotAuthorizedErrorResponseContent { + return NotAuthorizedErrorResponseContentFromJSONTyped(json, false); +} + +export function NotAuthorizedErrorResponseContentFromJSONTyped(json: any, ignoreDiscriminator: boolean): NotAuthorizedErrorResponseContent { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'message': json['message'], + }; +} + +export function NotAuthorizedErrorResponseContentToJSON(value?: NotAuthorizedErrorResponseContent | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'message': value.message, + }; +} + +", + "src/models/SuccessResponseContent.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ +import { exists, mapValues } from './model-utils.js'; + +/** + * + * @export + * @interface SuccessResponseContent + */ +export interface SuccessResponseContent { + /** + * + * @type {string} + * @memberof SuccessResponseContent + */ + message: string; +} + + +/** + * Check if a given object implements the SuccessResponseContent interface. + */ +export function instanceOfSuccessResponseContent(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "message" in value; + return isInstance; +} + +export function SuccessResponseContentFromJSON(json: any): SuccessResponseContent { + return SuccessResponseContentFromJSONTyped(json, false); +} + +export function SuccessResponseContentFromJSONTyped(json: any, ignoreDiscriminator: boolean): SuccessResponseContent { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'message': json['message'], + }; +} + +export function SuccessResponseContentToJSON(value?: SuccessResponseContent | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'message': value.message, + }; +} + +", + "src/models/index.ts": "/* tslint:disable */ +/* eslint-disable */ +export * from './BadRequestErrorResponseContent.js'; +export * from './InternalFailureErrorResponseContent.js'; +export * from './NotAuthorizedErrorResponseContent.js'; +export * from './SuccessResponseContent.js'; +", + "src/models/model-utils.ts": "/* tslint:disable */ +/* eslint-disable */ + +export function mapValues(data: any, fn: (item: any) => any) { + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} +", + "src/response/response.ts": "import { OperationResponse } from '../index.js'; + + +/** + * Helpers for constructing api responses + */ +export class Response { + /** + * A successful response + */ + public static success = ( + body: T + ): OperationResponse<200, T> => ({ + statusCode: 200, + body, + }); + + /** + * A response which indicates a client error + */ + public static badRequest = ( + body: T + ): OperationResponse<400, T> => ({ + statusCode: 400, + body, + }); + + /** + * A response which indicates the requested resource was not found + */ + public static notFound = ( + body: T + ): OperationResponse<404, T> => ({ + statusCode: 404, + body, + }); + + /** + * A response which indicates the caller is not authorised to perform the operation or access the resource + */ + public static notAuthorized = ( + body: T + ): OperationResponse<403, T> => ({ + statusCode: 403, + body, + }); + + /** + * A response to indicate a server error + */ + public static internalFailure = ( + body: T + ): OperationResponse<500, T> => ({ + statusCode: 500, + body, + }); +} +", + "src/runtime.ts": "/* tslint:disable */ +/* eslint-disable */ +/** + * \${options.openApiOptions.title} + * + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated. + * Do not edit the class manually. + */ + +export const BASE_PATH = "http://localhost".replace(/\\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | ((name: string) => string); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + const init: RequestInit = { + ...overriddenInit, + body: + isFormData(overriddenInit.body) || + overriddenInit.body instanceof URLSearchParams || + isBlob(overriddenInit.body) + ? overriddenInit.body + : JSON.stringify(overriddenInit.body), + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of \`this\` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? \`[\${key}]\` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(\`&\${encodeURIComponent(fullKey)}=\`); + return \`\${encodeURIComponent(fullKey)}=\${multiValue}\`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return \`\${encodeURIComponent(fullKey)}=\${encodeURIComponent(value.toISOString())}\`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return \`\${encodeURIComponent(fullKey)}=\${encodeURIComponent(String(value))}\`; +} + +export function mapValues(data: any, fn: (item: any) => any) { + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +} +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-async-cdk-infrastructure with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "src/api.ts +src/functions.ts +src/index.ts +src/mock-integrations.ts", + "src/api.ts": "import { TypeSafeWebsocketApi, TypeSafeWebsocketApiProps, TypeSafeWebsocketApiIntegration } from "@aws/pdk/type-safe-api"; +import { Construct } from "constructs"; +import { OperationConfig, OperationLookup } from "@test/runtime"; +import * as url from 'url'; + +export type WebSocketApiIntegrations = OperationConfig; + +export interface WebSocketApiProps extends Omit { + readonly integrations: WebSocketApiIntegrations; +} + +/** + * Type-safe construct for the API Gateway resources defined by your model. + * This construct is generated and should not be modified. + */ +export class WebSocketApi extends TypeSafeWebsocketApi { + constructor(scope: Construct, id: string, props: WebSocketApiProps) { + super(scope, id, { + ...props, + integrations: props.integrations as any, + operationLookup: OperationLookup, + specPath: url.fileURLToPath(new URL("spec.yaml", import.meta.url)), + }); + } +} +", + "src/functions.ts": "import { Construct } from "constructs"; +import { Duration } from "aws-cdk-lib"; +import { SnapStartFunction, SnapStartFunctionProps } from "@aws/pdk/type-safe-api"; +import { Code, Function, Runtime, Tracing, FunctionProps } from "aws-cdk-lib/aws-lambda"; +import * as path from "path"; +import * as url from 'url'; + +/** + * Options for the $ConnectFunction construct + */ +export interface $ConnectFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the typescript implementation for the websocket connect event + */ +export class $ConnectFunction extends Function { + constructor(scope: Construct, id: string, props?: $ConnectFunctionProps) { + super(scope, id, { + runtime: Runtime.NODEJS_20_X, + handler: "index.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/ts/dist", + "$connect", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the $DisconnectFunction construct + */ +export interface $DisconnectFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the java implementation for the websocket disconnect event + */ +export class $DisconnectFunction extends SnapStartFunction { + constructor(scope: Construct, id: string, props?: $DisconnectFunctionProps) { + super(scope, id, { + runtime: Runtime.JAVA_21, + handler: "com.java.test.$DisconnectHandler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/java/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the JavaOneFunction construct + */ +export interface JavaOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the java implementation of JavaOne + */ +export class JavaOneFunction extends SnapStartFunction { + constructor(scope: Construct, id: string, props?: JavaOneFunctionProps) { + super(scope, id, { + runtime: Runtime.JAVA_21, + handler: "com.java.test.JavaOneHandler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/java/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + + +/** + * Options for the JavaTwoFunction construct + */ +export interface JavaTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the java implementation of JavaTwo + */ +export class JavaTwoFunction extends SnapStartFunction { + constructor(scope: Construct, id: string, props?: JavaTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.JAVA_21, + handler: "com.java.test.JavaTwoHandler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/java/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + + +/** + * Options for the PythonOneFunction construct + */ +export interface PythonOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the python implementation of PythonOne + */ +export class PythonOneFunction extends Function { + constructor(scope: Construct, id: string, props?: PythonOneFunctionProps) { + super(scope, id, { + runtime: Runtime.PYTHON_3_12, + handler: "test_py.python_one.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/py/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + + +/** + * Options for the PythonTwoFunction construct + */ +export interface PythonTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the python implementation of PythonTwo + */ +export class PythonTwoFunction extends Function { + constructor(scope: Construct, id: string, props?: PythonTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.PYTHON_3_12, + handler: "test_py.python_two.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/py/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + + +/** + * Options for the TypescriptOneFunction construct + */ +export interface TypescriptOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the typescript implementation of TypescriptOne + */ +export class TypescriptOneFunction extends Function { + constructor(scope: Construct, id: string, props?: TypescriptOneFunctionProps) { + super(scope, id, { + runtime: Runtime.NODEJS_20_X, + handler: "index.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/ts/dist", + "typescript-one", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + + +/** + * Options for the TypescriptTwoFunction construct + */ +export interface TypescriptTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the typescript implementation of TypescriptTwo + */ +export class TypescriptTwoFunction extends Function { + constructor(scope: Construct, id: string, props?: TypescriptTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.NODEJS_20_X, + handler: "index.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/ts/dist", + "typescript-two", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +", + "src/index.ts": "export * from "./api.js"; +export * from "./functions.js"; +export * from "./mock-integrations.js";", + "src/mock-integrations.ts": "import { + WebSocketMockIntegration, +} from "aws-cdk-lib/aws-apigatewayv2-integrations"; + + +/** + * Type-safe mock integrations for WebSocket API operations + */ +export class MockIntegrations { + + /** + * Mock all operations + */ + public static mockAll() { + return { + javaOne: { + integration: new WebSocketMockIntegration("MockJavaOneIntegration"), + }, + javaTwo: { + integration: new WebSocketMockIntegration("MockJavaTwoIntegration"), + }, + pythonOne: { + integration: new WebSocketMockIntegration("MockPythonOneIntegration"), + }, + pythonTwo: { + integration: new WebSocketMockIntegration("MockPythonTwoIntegration"), + }, + typescriptOne: { + integration: new WebSocketMockIntegration("MockTypescriptOneIntegration"), + }, + typescriptTwo: { + integration: new WebSocketMockIntegration("MockTypescriptTwoIntegration"), + }, + }; + } +} +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-async-lambda-handlers with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "", + "src/$connect.ts": "import { + $connectHandler, + $ConnectChainedLambdaHandlerFunction, + INTERCEPTORS, + LoggingInterceptor, + $PendingConnection, +} from "@test/runtime"; + +/** + * Type-safe handler for the $connect event, invoked when a new client connects to the websocket + */ +export const $connect: $ConnectChainedLambdaHandlerFunction = async (request) => { + LoggingInterceptor.getLogger(request).info("Start $connect"); + + // \`connectionId\` is the ID of the new connection + // \`sdk\` is used to send messages to connected clients + // Note that you cannot send messages to the new connection until after this function returns + const { connectionId, sdk } = request; + + // TODO: Implement + + // Use the below to allow or deny the incoming connection (when the lambda returns). + // The connection is allowed by default. + $PendingConnection.of(request).allow(); +}; + +/** + * Entry point for the AWS Lambda handler for the $connect event. + * The $connectHandler method wraps the type-safe handler and manages marshalling inputs + */ +export const handler = $connectHandler(...INTERCEPTORS, $connect); + +", + "src/typescript-one.ts": "import { + typescriptOneHandler, + TypescriptOneChainedHandlerFunction, + INTERCEPTORS, + LoggingInterceptor, +} from "@test/runtime"; + +/** + * Type-safe handler for the TypescriptOne operation + */ +export const typescriptOne: TypescriptOneChainedHandlerFunction = async (request) => { + LoggingInterceptor.getLogger(request).info("Start TypescriptOne Operation"); + + // \`input\` contains the request input + // \`connectionId\` is the ID of the connection which sent this request to the server. + // \`sdk\` is used to send messages to connected clients + const { input, connectionId, sdk } = request; + + // TODO: Implement TypescriptOne Operation. +}; + +/** + * Entry point for the AWS Lambda handler for the TypescriptOne operation. + * The typescriptOneHandler method wraps the type-safe handler and manages marshalling inputs + */ +export const handler = typescriptOneHandler(...INTERCEPTORS, typescriptOne); + +", + "src/typescript-two.ts": "import { + typescriptTwoHandler, + TypescriptTwoChainedHandlerFunction, + INTERCEPTORS, + LoggingInterceptor, +} from "@test/runtime"; + +/** + * Type-safe handler for the TypescriptTwo operation + */ +export const typescriptTwo: TypescriptTwoChainedHandlerFunction = async (request) => { + LoggingInterceptor.getLogger(request).info("Start TypescriptTwo Operation"); + + // \`input\` contains the request input + // \`connectionId\` is the ID of the connection which sent this request to the server. + // \`sdk\` is used to send messages to connected clients + const { input, connectionId, sdk } = request; + + // TODO: Implement TypescriptTwo Operation. +}; + +/** + * Entry point for the AWS Lambda handler for the TypescriptTwo operation. + * The typescriptTwoHandler method wraps the type-safe handler and manages marshalling inputs + */ +export const handler = typescriptTwoHandler(...INTERCEPTORS, typescriptTwo); + +", + "test/typescript-one.test.ts": "import { + TypescriptOneChainedRequestInput, +} from "@test/runtime"; +import { + typescriptOne +} from "../src/typescript-one.js"; + +// Common request arguments +const requestArguments = { + chain: undefined as never, + connectionId: 'test', + sdk: {} as any, + event: {} as any, + context: {} as any, + interceptorContext: { + logger: { + info: jest.fn(), + }, + }, +} satisfies Omit; + +describe('TypescriptOne', () => { + + it('should not throw', async () => { + // TODO: Update the test as appropriate when you implement your handler + await typescriptOne({ + ...requestArguments, + // TODO: remove the "as any" below and fill in test values for the input + input: {} as any, + }); + }); + +}); + +", + "test/typescript-two.test.ts": "import { + TypescriptTwoChainedRequestInput, +} from "@test/runtime"; +import { + typescriptTwo +} from "../src/typescript-two.js"; + +// Common request arguments +const requestArguments = { + chain: undefined as never, + connectionId: 'test', + sdk: {} as any, + event: {} as any, + context: {} as any, + interceptorContext: { + logger: { + info: jest.fn(), + }, + }, +} satisfies Omit; + +describe('TypescriptTwo', () => { + + it('should not throw', async () => { + // TODO: Update the test as appropriate when you implement your handler + await typescriptTwo({ + ...requestArguments, + // TODO: remove the "as any" below and fill in test values for the input + input: {} as any, + }); + }); + +}); + +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-async-runtime with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "src/index.ts +src/interceptors/try-catch.ts +src/interceptors/powertools/logger.ts +src/interceptors/powertools/tracer.ts +src/interceptors/powertools/metrics.ts +src/interceptors/index.ts +src/server/operation-config.ts +src/server/server-sdk.ts", + "src/index.ts": "export * from './models/index.js'; +export * from './server/operation-config.js'; +export * from './server/server-sdk.js'; +export * from './interceptors/index.js' +", + "src/interceptors/index.ts": "import { LoggingInterceptor } from './powertools/logger.js'; +import { MetricsInterceptor } from './powertools/metrics.js'; +import { TracingInterceptor } from './powertools/tracer.js'; +import { tryCatchInterceptor } from './try-catch.js'; + +export * from './try-catch.js'; +export * from './powertools/tracer.js'; +export * from './powertools/metrics.js'; +export * from './powertools/logger.js'; + +/** + * All default interceptors, for logging, tracing, metrics, and error handling + */ +export const INTERCEPTORS = [ + LoggingInterceptor.intercept, + tryCatchInterceptor, + TracingInterceptor.intercept, + MetricsInterceptor.intercept, +] as const; +", + "src/interceptors/powertools/logger.ts": "import { Logger } from '@aws-lambda-powertools/logger'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../../index.js'; + +const logger = new Logger(); + +export class LoggingInterceptor { + /** + * Interceptor which adds an aws lambda powertools logger to the interceptor context, + * and adds the lambda context + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/ + */ + public static intercept = async ( + request: PayloadlessChainedRequestInput, + ): Promise => { + logger.addContext(request.context); + logger.appendKeys({ operationId: request.interceptorContext.operationId }); + request.interceptorContext.logger = logger; + const response = await request.chain.next(request); + logger.removeKeys(['operationId']); + return response; + }; + + /** + * Retrieve the logger from the interceptor context + */ + public static getLogger = (request: PayloadlessChainedRequestInput | ChainedRequestInput): Logger => { + if (!request.interceptorContext.logger) { + throw new Error('No logger found, did you configure the LoggingInterceptor?'); + } + return request.interceptorContext.logger; + }; +} +", + "src/interceptors/powertools/metrics.ts": "import { Metrics } from '@aws-lambda-powertools/metrics'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../../index.js'; + +const metrics = new Metrics(); + +export class MetricsInterceptor { + /** + * Interceptor which adds an instance of aws lambda powertools metrics to the interceptor context, + * and ensures metrics are flushed prior to finishing the lambda execution + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/ + */ + public static intercept = async ( + request: PayloadlessChainedRequestInput, + ): Promise => { + metrics.addDimension("operationId", request.interceptorContext.operationId); + request.interceptorContext.metrics = metrics; + try { + return await request.chain.next(request); + } finally { + // Flush metrics + metrics.publishStoredMetrics(); + } + }; + + /** + * Retrieve the metrics logger from the request + */ + public static getMetrics = ( + request: PayloadlessChainedRequestInput | ChainedRequestInput, + ): Metrics => { + if (!request.interceptorContext.metrics) { + throw new Error('No metrics logger found, did you configure the MetricsInterceptor?'); + } + return request.interceptorContext.metrics; + }; +} +", + "src/interceptors/powertools/tracer.ts": "import { Tracer } from '@aws-lambda-powertools/tracer'; +import { PayloadlessChainedRequestInput, ChainedRequestInput } from '../../index.js'; + +const tracer = new Tracer(); + +/** + * Create an interceptor which adds an aws lambda powertools tracer to the interceptor context, + * creating the appropriate segment for the handler execution and annotating with recommended + * details. + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/tracer/#lambda-handler + */ +export const buildTracingInterceptor = () => async ( + request: PayloadlessChainedRequestInput, +): Promise => { + const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler'; + const segment = tracer.getSegment(); + let subsegment; + if (segment) { + subsegment = segment.addNewSubsegment(handler); + tracer.setSegment(subsegment); + } + + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + if (request.interceptorContext.logger) { + tracer.provider.setLogger(request.interceptorContext.logger); + } + + request.interceptorContext.tracer = tracer; + + try { + return await request.chain.next(request); + } catch (e) { + tracer.addErrorAsMetadata(e as Error); + throw e; + } finally { + if (segment && subsegment) { + subsegment.close(); + tracer.setSegment(segment); + } + } +}; + +export class TracingInterceptor { + /** + * Interceptor which adds an aws lambda powertools tracer to the interceptor context, + * creating the appropriate segment for the handler execution and annotating with recommended + * details. + */ + public static intercept = buildTracingInterceptor(); + + /** + * Get the tracer from the interceptor context + */ + public static getTracer = ( + request: PayloadlessChainedRequestInput | ChainedRequestInput, + ): Tracer => { + if (!request.interceptorContext.tracer) { + throw new Error('No tracer found, did you configure the TracingInterceptor?'); + } + return request.interceptorContext.tracer; + }; +} +", + "src/interceptors/try-catch.ts": "import { + PayloadlessChainedRequestInput, +} from '../index.js'; + +/** + * Create an interceptor which catches any unhandled exceptions + */ +export const buildTryCatchInterceptor = () => async ( + request: PayloadlessChainedRequestInput, +): Promise => { + try { + return await request.chain.next(request); + } catch (e: any) { + // Log the error if the logger is present + if (request.interceptorContext.logger && request.interceptorContext.logger.error) { + request.interceptorContext.logger.error('Interceptor caught exception', e as Error); + } else { + console.error('Interceptor caught exception', e); + } + } +}; + +/** + * Interceptor for catching unhandled exceptions + */ +export const tryCatchInterceptor = buildTryCatchInterceptor(); +", + "src/server/operation-config.ts": "// Import models +import { + Request, + RequestFromJSON, + RequestToJSON, +} from '../models/index.js'; + +// API Gateway Types +import { APIGatewayProxyWebsocketEventV2, APIGatewayProxyResultV2, Context } from "aws-lambda"; +import { DefaultApiServerSdk } from "./server-sdk.js"; + +// Generic type for object keyed by operation names +export interface OperationConfig { + javaOne: T; + javaTwo: T; + pythonOne: T; + pythonTwo: T; + typescriptOne: T; + typescriptTwo: T; +} + +// Look up path and http method for a given operation name +export const OperationLookup = { + javaOne: { + path: '/JavaOne', + method: 'POST', + contentTypes: ['application/json'], + }, + javaTwo: { + path: '/JavaTwo', + method: 'POST', + contentTypes: ['application/json'], + }, + pythonOne: { + path: '/PythonOne', + method: 'POST', + contentTypes: ['application/json'], + }, + pythonTwo: { + path: '/PythonTwo', + method: 'POST', + contentTypes: ['application/json'], + }, + typescriptOne: { + path: '/TypeScriptOne', + method: 'POST', + contentTypes: ['application/json'], + }, + typescriptTwo: { + path: '/TypeScriptTwo', + method: 'POST', + contentTypes: ['application/json'], + }, +}; + +export class Operations { + /** + * Return an OperationConfig with the same value for every operation + */ + public static all = (value: T): OperationConfig => Object.fromEntries( + Object.keys(OperationLookup).map((operationId) => [operationId, value]) + ) as unknown as OperationConfig; +} + +/** + * Parse the body if the content type is json and return the payload, otherwise leave as a raw string + */ +const parseBody = (body: string, demarshal: (body: string) => any, contentTypes: string[]): any => contentTypes.filter((contentType) => contentType !== 'application/json').length === 0 ? demarshal(body || '{}') : body; + +/** + * Utilities for the $connect route to allow or deny a pending connection + */ +export class $PendingConnection { + public static DENY_CONTEXT_KEY = '$PendingConnectionDenied'; + + public static of = (request: PayloadlessChainedRequestInput) => { + return new $PendingConnection(request.interceptorContext); + }; + + private constructor(private interceptorContext: Record) {} + + /** + * Allows the connection (not immediate, takes effect when the lambda handler returns) + */ + public allow = () => { + this.interceptorContext[$PendingConnection.DENY_CONTEXT_KEY] = false; + }; + + /** + * Denies the connection (not immediate, takes effect when the lambda handler returns) + */ + public deny = () => { + this.interceptorContext[$PendingConnection.DENY_CONTEXT_KEY] = true; + }; +} + +export type OperationIds = "$connect" | "$disconnect" | 'javaOne' | 'javaTwo' | 'pythonOne' | 'pythonTwo' | 'typescriptOne' | 'typescriptTwo'; +export type OperationApiGatewayProxyResult = APIGatewayProxyResultV2 & { __operationId?: T }; + +// Api gateway lambda handler type +export type OperationApiGatewayLambdaHandler = (event: APIGatewayProxyWebsocketEventV2, context: Context) => Promise>; + +export type InterceptorContext = { [key: string]: any }; + +export interface PayloadlessRequestInput { + connectionId: string; + sdk: DefaultApiServerSdk; + event: APIGatewayProxyWebsocketEventV2; + context: Context; + interceptorContext: InterceptorContext; +} + +export interface RequestInput extends PayloadlessRequestInput { + input: RequestBody; +} + +export interface PayloadlessChainedRequestInput extends PayloadlessRequestInput { + chain: PayloadlessLambdaHandlerChain; +} + +export interface ChainedRequestInput extends RequestInput { + chain: LambdaHandlerChain; +} + +export type PayloadlessChainedLambdaHandlerFunction = ( + input: PayloadlessChainedRequestInput +) => Promise; + +/** + * A lambda handler function which is part of a chain. It may invoke the remainder of the chain via the given chain input + */ +export type ChainedLambdaHandlerFunction = ( + input: ChainedRequestInput, +) => Promise; + +export type PayloadlessLambdaHandlerFunction = ( + input: PayloadlessRequestInput +) => Promise; + +// Type for a lambda handler function to be wrapped +export type LambdaHandlerFunction = ( + input: RequestInput, +) => Promise; + +export interface PayloadlessLambdaHandlerChain { + next: PayloadlessLambdaHandlerFunction; +} + +export interface LambdaHandlerChain { + next: LambdaHandlerFunction; +} + +// Interceptor is a type alias for ChainedLambdaHandlerFunction +export type Interceptor = ChainedLambdaHandlerFunction; + +/** + * Build a chain from the given array of chained lambda handlers + */ +const buildHandlerChain = ( + ...handlers: ChainedLambdaHandlerFunction[] +): LambdaHandlerChain => { + if (handlers.length === 0) { + return { + next: () => { + throw new Error("No more handlers remain in the chain! The last handler should not call next."); + } + }; + } + const [currentHandler, ...remainingHandlers] = handlers; + return { + next: (input) => { + return currentHandler({ + ...input, + chain: buildHandlerChain(...remainingHandlers), + }); + }, + }; +}; + + +/** + * Request body parameter for JavaOne + */ +export type JavaOneRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type JavaOneHandlerFunction = LambdaHandlerFunction; +export type JavaOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type JavaOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of javaOne + */ +export const javaOneHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | JavaOneChainedHandlerFunction)[], JavaOneChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'javaOne'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | JavaOneChainedHandlerFunction)[] = []): Promise => { + const operationId = "javaOne"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as JavaOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +/** + * Request body parameter for JavaTwo + */ +export type JavaTwoRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type JavaTwoHandlerFunction = LambdaHandlerFunction; +export type JavaTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type JavaTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of javaTwo + */ +export const javaTwoHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | JavaTwoChainedHandlerFunction)[], JavaTwoChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'javaTwo'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | JavaTwoChainedHandlerFunction)[] = []): Promise => { + const operationId = "javaTwo"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as JavaTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +/** + * Request body parameter for PythonOne + */ +export type PythonOneRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type PythonOneHandlerFunction = LambdaHandlerFunction; +export type PythonOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type PythonOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of pythonOne + */ +export const pythonOneHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | PythonOneChainedHandlerFunction)[], PythonOneChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'pythonOne'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | PythonOneChainedHandlerFunction)[] = []): Promise => { + const operationId = "pythonOne"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as PythonOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +/** + * Request body parameter for PythonTwo + */ +export type PythonTwoRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type PythonTwoHandlerFunction = LambdaHandlerFunction; +export type PythonTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type PythonTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of pythonTwo + */ +export const pythonTwoHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | PythonTwoChainedHandlerFunction)[], PythonTwoChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'pythonTwo'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | PythonTwoChainedHandlerFunction)[] = []): Promise => { + const operationId = "pythonTwo"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as PythonTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +/** + * Request body parameter for TypescriptOne + */ +export type TypescriptOneRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type TypescriptOneHandlerFunction = LambdaHandlerFunction; +export type TypescriptOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type TypescriptOneChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of typescriptOne + */ +export const typescriptOneHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | TypescriptOneChainedHandlerFunction)[], TypescriptOneChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'typescriptOne'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | TypescriptOneChainedHandlerFunction)[] = []): Promise => { + const operationId = "typescriptOne"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as TypescriptOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +/** + * Request body parameter for TypescriptTwo + */ +export type TypescriptTwoRequestBody = Request; + +// Type that the handler function provided to the wrapper must conform to +export type TypescriptTwoHandlerFunction = LambdaHandlerFunction; +export type TypescriptTwoChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type TypescriptTwoChainedRequestInput = ChainedRequestInput; + +/** + * Lambda handler wrapper to provide typed interface for the implementation of typescriptTwo + */ +export const typescriptTwoHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | TypescriptTwoChainedHandlerFunction)[], TypescriptTwoChainedHandlerFunction] +): OperationApiGatewayLambdaHandler<'typescriptTwo'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: (PayloadlessChainedLambdaHandlerFunction | TypescriptTwoChainedHandlerFunction)[] = []): Promise => { + const operationId = "typescriptTwo"; + + const demarshal = (bodyString: string): any => { + return RequestFromJSON(JSON.parse(bodyString)?.payload ?? {}); + }; + const body = parseBody(event.body, demarshal, ['application/json']) as TypescriptTwoRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + await chain.next({ + input: body, + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +export type $ConnectChainedLambdaHandlerFunction = PayloadlessChainedLambdaHandlerFunction; + +export const $connectHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | ChainedLambdaHandlerFunction)[], $ConnectChainedLambdaHandlerFunction] +): OperationApiGatewayLambdaHandler<'$connect'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: PayloadlessChainedLambdaHandlerFunction[] = []): Promise => { + const operationId = "$connect"; + const chain = buildHandlerChain(...additionalInterceptors, ...(handlers as any)) as PayloadlessLambdaHandlerChain; + const interceptorContext = { operationId }; + await chain.next({ + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext, + }); + + return { + // Respond with 200, unless the handler explicitly denies the connection + statusCode: interceptorContext[$PendingConnection.DENY_CONTEXT_KEY] ? 403 : 200, + }; +}; + +export type $DisconnectChainedLambdaHandlerFunction = PayloadlessChainedLambdaHandlerFunction; + +export const $disconnectHandler = ( + ...handlers: [...(PayloadlessChainedLambdaHandlerFunction | ChainedLambdaHandlerFunction)[], $DisconnectChainedLambdaHandlerFunction] +): OperationApiGatewayLambdaHandler<'$disconnect'> => async (event: APIGatewayProxyWebsocketEventV2, context: any, _callback?: any, additionalInterceptors: PayloadlessChainedLambdaHandlerFunction[] = []): Promise => { + const operationId = "$disconnect"; + const chain = buildHandlerChain(...additionalInterceptors, ...handlers) as PayloadlessLambdaHandlerChain; + await chain.next({ + connectionId: event.requestContext.connectionId, + sdk: new DefaultApiServerSdk({ + callbackUrl: \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`, + }), + event, + context, + interceptorContext: { operationId }, + }); + + return { + // Respond success to indicate to API gateway that we have implemented the integration + // Websocket operations are one-way. + statusCode: 200, + }; +}; + +export interface HandlerRouterHandlers { + readonly javaOne: OperationApiGatewayLambdaHandler<'javaOne'>; + readonly javaTwo: OperationApiGatewayLambdaHandler<'javaTwo'>; + readonly pythonOne: OperationApiGatewayLambdaHandler<'pythonOne'>; + readonly pythonTwo: OperationApiGatewayLambdaHandler<'pythonTwo'>; + readonly typescriptOne: OperationApiGatewayLambdaHandler<'typescriptOne'>; + readonly typescriptTwo: OperationApiGatewayLambdaHandler<'typescriptTwo'>; + readonly $connect?: OperationApiGatewayLambdaHandler<'$connect'>; + readonly $disconnect?: OperationApiGatewayLambdaHandler<'$disconnect'>; +} + +export type AnyOperationRequestBodies = | JavaOneRequestBody| JavaTwoRequestBody| PythonOneRequestBody| PythonTwoRequestBody| TypescriptOneRequestBody| TypescriptTwoRequestBody; + +export interface HandlerRouterProps< + RequestBody, +> { + /** + * Interceptors to apply to all handlers + */ + readonly interceptors?: ReadonlyArray<(PayloadlessChainedLambdaHandlerFunction | ChainedLambdaHandlerFunction)>; + + /** + * Handlers to register for each operation + */ + readonly handlers: HandlerRouterHandlers; +} + +const OperationIdByPath = Object.fromEntries(Object.entries(OperationLookup).map( + ([operationId, details]: [string, {path: string}]) => [details.path.replace(/\\//g, ''), operationId] +)); + +/** + * Returns a lambda handler which can be used to route requests to the appropriate typed lambda handler function. + */ +export const handlerRouter = (props: HandlerRouterProps< + AnyOperationRequestBodies +>): OperationApiGatewayLambdaHandler => async (event, context) => { + const handler = props.handlers[OperationIdByPath[event.requestContext.routeKey]]; + return handler(event, context, undefined, props.interceptors); +}; +", + "src/server/server-sdk.ts": "// Import models +import { + Request, + RequestFromJSON, + RequestToJSON, +} from "../models/index.js"; +import { + ApiGatewayManagementApiClient, + PostToConnectionCommand, + DeleteConnectionCommand, +} from "@aws-sdk/client-apigatewaymanagementapi"; + +/** + * Options for the server SDK + */ +export interface DefaultApiServerSdkProps { + /** + * API Gateway management API callback url + */ + readonly callbackUrl: string; +} + +/** + * SDK for sending messages from the server to connected clients + */ +export class DefaultApiServerSdk { + private readonly client: ApiGatewayManagementApiClient; + + constructor(props: DefaultApiServerSdkProps) { + this.client = new ApiGatewayManagementApiClient({ endpoint: props.callbackUrl }); + } + + public async javaOne(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "JavaOne", + payload: input, + }), + })); + } + public async javaTwo(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "JavaTwo", + payload: input, + }), + })); + } + public async pythonOne(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "PythonOne", + payload: input, + }), + })); + } + public async pythonTwo(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "PythonTwo", + payload: input, + }), + })); + } + public async typescriptOne(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "TypescriptOne", + payload: input, + }), + })); + } + public async typescriptTwo(connectionId: string, input: Request): Promise { + await this.client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: JSON.stringify({ + route: "TypescriptTwo", + payload: input, + }), + })); + } + + /** + * Disconnect a connected client + */ + public async $disconnect(connectionId: string): Promise { + await this.client.send(new DeleteConnectionCommand({ + ConnectionId: connectionId, + })); + } +} +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-cdk-infrastructure with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "src/api.ts +src/functions.ts +src/index.ts +src/mock-integrations.ts", + "src/api.ts": "import { TypeSafeRestApi, TypeSafeRestApiProps, TypeSafeApiIntegration } from "@aws/pdk/type-safe-api"; +import { Construct } from "constructs"; +import { OperationLookup, OperationConfig } from "@test/runtime"; +import * as url from 'url'; + +export type ApiIntegrations = OperationConfig; + +export interface ApiProps extends Omit { + readonly integrations: ApiIntegrations; +} + +/** + * Type-safe construct for the API Gateway resources defined by your model. + * This construct is generated and should not be modified. + */ +export class Api extends TypeSafeRestApi { + constructor(scope: Construct, id: string, props: ApiProps) { + super(scope, id, { + ...props, + integrations: props.integrations as any, + specPath: url.fileURLToPath(new URL("spec.yaml", import.meta.url)), + operationLookup: OperationLookup as any, + }); + } +} +", + "src/functions.ts": "import { Construct } from "constructs"; +import { Duration } from "aws-cdk-lib"; +import { SnapStartFunction, SnapStartFunctionProps } from "@aws/pdk/type-safe-api"; +import { Code, Function, Runtime, Tracing, FunctionProps } from "aws-cdk-lib/aws-lambda"; +import * as path from "path"; +import * as url from 'url'; + +/** + * Options for the JavaOneFunction construct + */ +export interface JavaOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the java implementation of JavaOne + */ +export class JavaOneFunction extends SnapStartFunction { + constructor(scope: Construct, id: string, props?: JavaOneFunctionProps) { + super(scope, id, { + runtime: Runtime.JAVA_21, + handler: "com.java.test.JavaOneHandler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/java/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the JavaTwoFunction construct + */ +export interface JavaTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the java implementation of JavaTwo + */ +export class JavaTwoFunction extends SnapStartFunction { + constructor(scope: Construct, id: string, props?: JavaTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.JAVA_21, + handler: "com.java.test.JavaTwoHandler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/java/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the PythonOneFunction construct + */ +export interface PythonOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the python implementation of PythonOne + */ +export class PythonOneFunction extends Function { + constructor(scope: Construct, id: string, props?: PythonOneFunctionProps) { + super(scope, id, { + runtime: Runtime.PYTHON_3_12, + handler: "test_py.python_one.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/py/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the PythonTwoFunction construct + */ +export interface PythonTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the python implementation of PythonTwo + */ +export class PythonTwoFunction extends Function { + constructor(scope: Construct, id: string, props?: PythonTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.PYTHON_3_12, + handler: "test_py.python_two.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/py/dist", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the TypescriptOneFunction construct + */ +export interface TypescriptOneFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the typescript implementation of TypescriptOne + */ +export class TypescriptOneFunction extends Function { + constructor(scope: Construct, id: string, props?: TypescriptOneFunctionProps) { + super(scope, id, { + runtime: Runtime.NODEJS_20_X, + handler: "index.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/ts/dist", + "typescript-one", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +/** + * Options for the TypescriptTwoFunction construct + */ +export interface TypescriptTwoFunctionProps extends Omit {} + +/** + * Lambda function construct which points to the typescript implementation of TypescriptTwo + */ +export class TypescriptTwoFunction extends Function { + constructor(scope: Construct, id: string, props?: TypescriptTwoFunctionProps) { + super(scope, id, { + runtime: Runtime.NODEJS_20_X, + handler: "index.handler", + code: Code.fromAsset(url.fileURLToPath(new URL(path.join("..", + "test/ts/dist", + "typescript-two", + ), import.meta.url))), + tracing: Tracing.ACTIVE, + timeout: Duration.seconds(30), + ...props, + }); + } +} + +", + "src/index.ts": "export * from "./api.js"; +export * from "./mock-integrations.js"; +export * from "./functions.js"; +", + "src/mock-integrations.ts": "import { + BadRequestErrorResponseContent, + BadRequestErrorResponseContentToJSON, + InternalFailureErrorResponseContent, + InternalFailureErrorResponseContentToJSON, + NotAuthorizedErrorResponseContent, + NotAuthorizedErrorResponseContentToJSON, + SuccessResponseContent, + SuccessResponseContentToJSON, +} from "@test/runtime"; +import { Integrations, MockIntegration } from "@aws/pdk/type-safe-api"; +import * as fs from "fs"; +import * as path from "path"; +import * as url from 'url'; + +/** + * Type-safe mock integrations for API operations + */ +export class MockIntegrations { + /** + * Read a mock data file for the given operation + */ + private static readMockDataFile(method: string, urlPath: string, statusCode: number): string { + const mockPath = path.join("..", "mocks", \`\${method.toLowerCase()}\${urlPath.replace(/\\//g, "-")}-\${statusCode}.json\`); + return fs.readFileSync( + url.fileURLToPath(new URL(mockPath, import.meta.url)), + "utf-8", + ); + } + + // No mock integrations have been generated, since mock data generation is disabled. + +} +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-lambda-handlers with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "", + "src/typescript-one.ts": "import { + typescriptOneHandler, + TypescriptOneChainedHandlerFunction, + INTERCEPTORS, + Response, + LoggingInterceptor, +} from "@test/runtime"; + +/** + * Type-safe handler for the TypescriptOne operation + */ +export const typescriptOne: TypescriptOneChainedHandlerFunction = async (request) => { + LoggingInterceptor.getLogger(request).info("Start TypescriptOne Operation"); + + // TODO: Implement TypescriptOne Operation. \`input\` contains the request input. + const { input } = request; + + return Response.internalFailure({ + message: "Not Implemented!", + }); +}; + +/** + * Entry point for the AWS Lambda handler for the TypescriptOne operation. + * The typescriptOneHandler method wraps the type-safe handler and manages marshalling inputs and outputs + */ +export const handler = typescriptOneHandler(...INTERCEPTORS, typescriptOne); +", + "src/typescript-two.ts": "import { + typescriptTwoHandler, + TypescriptTwoChainedHandlerFunction, + INTERCEPTORS, + Response, + LoggingInterceptor, +} from "@test/runtime"; + +/** + * Type-safe handler for the TypescriptTwo operation + */ +export const typescriptTwo: TypescriptTwoChainedHandlerFunction = async (request) => { + LoggingInterceptor.getLogger(request).info("Start TypescriptTwo Operation"); + + // TODO: Implement TypescriptTwo Operation. \`input\` contains the request input. + const { input } = request; + + return Response.internalFailure({ + message: "Not Implemented!", + }); +}; + +/** + * Entry point for the AWS Lambda handler for the TypescriptTwo operation. + * The typescriptTwoHandler method wraps the type-safe handler and manages marshalling inputs and outputs + */ +export const handler = typescriptTwoHandler(...INTERCEPTORS, typescriptTwo); +", + "test/typescript-one.test.ts": "import { + InternalFailureErrorResponseContent, + TypescriptOneChainedRequestInput, +} from "@test/runtime"; +import { + typescriptOne +} from "../src/typescript-one.js"; + +// Common request arguments +const requestArguments = { + chain: undefined as never, + event: {} as any, + context: {} as any, + interceptorContext: { + logger: { + info: jest.fn(), + }, + }, +} satisfies Omit; + +describe('TypescriptOne', () => { + + it('should return not implemented error', async () => { + // TODO: Update the test as appropriate when you implement your handler + const response = await typescriptOne({ + ...requestArguments, + input: { + // TODO: remove the "as any" below and fill in test values for the requestParameters + requestParameters: {} as any, + body: {} as never, + }, + }); + + expect(response.statusCode).toBe(500); + expect((response.body as InternalFailureErrorResponseContent).message).toEqual('Not Implemented!'); + }); + +}); + +", + "test/typescript-two.test.ts": "import { + InternalFailureErrorResponseContent, + TypescriptTwoChainedRequestInput, +} from "@test/runtime"; +import { + typescriptTwo +} from "../src/typescript-two.js"; + +// Common request arguments +const requestArguments = { + chain: undefined as never, + event: {} as any, + context: {} as any, + interceptorContext: { + logger: { + info: jest.fn(), + }, + }, +} satisfies Omit; + +describe('TypescriptTwo', () => { + + it('should return not implemented error', async () => { + // TODO: Update the test as appropriate when you implement your handler + const response = await typescriptTwo({ + ...requestArguments, + input: { + // TODO: remove the "as any" below and fill in test values for the requestParameters + requestParameters: {} as any, + body: {} as never, + }, + }); + + expect(response.statusCode).toBe(500); + expect((response.body as InternalFailureErrorResponseContent).message).toEqual('Not Implemented!'); + }); + +}); + +", +} +`; + +exports[`TypeScript ESM Generator Tests Generates typescript-websocket-client with ESM compatible syntax 1`] = ` +{ + ".tsapi-manifest": "src/client/client.ts +src/index.ts", + "src/client/client.ts": "// Import models +import { + Request, + RequestFromJSON, + RequestToJSON, +} from "../models/index.js"; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { HttpRequest } from "@aws-sdk/protocol-http"; +import { SignatureV4 } from "@aws-sdk/signature-v4"; +import { Sha256 } from "@aws-crypto/sha256-js"; +import { v4 as uuid } from "uuid"; + +import WebSocket from "isomorphic-ws"; + +export interface IamAuthenticationStrategy { + readonly region: string; + readonly credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider; +} + +export interface NoneAuthenticationStrategy {} + +export interface CustomAuthenticationStrategyInput { + readonly url: string; +} + +export interface CustomAuthenticationStrategyOutput { + readonly url: string; +} + +export interface CustomAuthenticationStrategy { + readonly apply: (input: CustomAuthenticationStrategyInput) => Promise; +} + +export type AuthenticationStrategy = { iam: IamAuthenticationStrategy } | { none: NoneAuthenticationStrategy } | { custom: CustomAuthenticationStrategy }; + +/** + * Options for the client + */ +export interface DefaultApiWebSocketClientOptions { + /** + * Websocket url to connect to (wss://xxxx) + */ + readonly url: string; + + /** + * Strategy to authenticate with the API + * @default AuthenticationStrategy.NONE + */ + readonly authentication?: AuthenticationStrategy; + + /** + * Maximum number of times to attempt to reconnect if connecting fails. + * @default 3 + */ + readonly maxRetries?: number; + + /** + * After this amount of time has elapsed, reset the number of retries. + * Ensures that stale connections closed by the browser (or node) are reconnected. + * @default 10000 + */ + readonly resetRetriesAfterMilliseconds?: number; +} + +enum SocketStatus { + CONNECTED = "CONNECTED", + DISCONNECTED = "DISCONNECTED", + CONNECTING = "CONNECTING", +} + +interface QueuedMessage { + readonly message: string; + readonly resolve: () => void; + readonly reject: () => void; +} + +interface MessageListener { + readonly id: string; + readonly listener: (payload: any) => void; +} + +interface ReconnectListener { + readonly id: string; + readonly listener: () => void; +} + +interface AllMessagesListener { + readonly id: string; + readonly listener: (route: string, payload?: any) => void; +} + +export interface WebSocketError { + readonly message: string; +} + +interface ErrorListener { + readonly id: string; + readonly listener: (error: WebSocketError) => void; +} + +/** + * Client for sending messages from clients to the server + */ +export class DefaultApiWebSocketClient { + /** + * Create a new WebSocket connection to the server + */ + public static connect = async (options: DefaultApiWebSocketClientOptions) => { + const client = new DefaultApiWebSocketClient(options); + await client.$connect(); + return client; + }; + + private readonly options: DefaultApiWebSocketClientOptions; + + private socket: WebSocket | undefined; + private status: SocketStatus = SocketStatus.CONNECTING; + private readonly messageQueue: QueuedMessage[] = []; + private listeners: { [route: string]: MessageListener[] } = {}; + private allMessageListeners: AllMessagesListener[] = []; + private reconnectListeners: ReconnectListener[] = []; + private errorListeners: ErrorListener[] = []; + private connectionAttempt: number = 0; + private lastConnected: number = Date.now(); + + private constructor(options: DefaultApiWebSocketClientOptions) { + this.options = options; + } + + private _signConnectionUrl = async (iam: IamAuthenticationStrategy) => { + const url = new URL(this.options.url); + + const request = new HttpRequest({ + hostname: url.hostname, + method: "GET", + path: url.pathname, + protocol: url.protocol, + headers: { + host: url.hostname, + }, + query: Object.fromEntries((url.searchParams ?? {}) as any), + }); + + const sigv4 = new SignatureV4({ + credentials: iam.credentials, + service: "execute-api", + region: iam.region, + sha256: Sha256, + }); + + const signedRequest = await sigv4.presign(request); + + Object.keys(signedRequest.query ?? {}).forEach((param) => { + const value = (signedRequest.query ?? {})[param]; + if (value) { + url.searchParams.set(param, value as any); + } + }); + + return url.toString(); + }; + + private _sleep = async (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }; + + private _onClose = async (_event: WebSocket.CloseEvent) => { + this.socket.onclose = null; + this.socket.onmessage = null; + this.socket.onerror = null; + + // After 10 seconds (or configured time), reset the number of retries so stale connections are always refreshed + if ((Date.now() - this.lastConnected) > (this.options.resetRetriesAfterMilliseconds ?? 10000)) { + this.connectionAttempt = 0; + } + + if (this.connectionAttempt >= (this.options.maxRetries ?? 3)) { + this._onDisconnect(); + const message = "Connection failed after maximum number of retries"; + this.errorListeners.forEach(({ listener }) => listener({ message })); + throw new Error(message); + } + + this.connectionAttempt++; + + await this._sleep(2 ** this.connectionAttempt * 10); + + await this._connect(); + + this.reconnectListeners.forEach(({ listener }) => listener()); + }; + + private _listen = (route: string, listener: (payload: any) => void): () => void => { + if (!this.listeners[route]) { + this.listeners[route] = []; + } + + const listenerId = uuid(); + this.listeners[route].push({ + id: listenerId, + listener, + }); + + return () => { + this.listeners[route] = this.listeners[route].filter(({ id }) => id !== listenerId); + }; + }; + + private _onMessage = async (event: WebSocket.MessageEvent) => { + if (typeof event.data !== "string" || !event.data) { + return; + } + + try { + const data = JSON.parse(event.data); + + if ('message' in data && typeof data.message === "string") { + this.errorListeners.forEach(({ listener }) => listener({ message: data.message })); + } else if ('route' in data) { + (this.listeners[data.route] ?? []).forEach(({ listener }) => listener(data.payload)); + this.allMessageListeners.forEach(({ listener }) => listener(data.route, data.payload)); + } else { + this.errorListeners.forEach(({ listener }) => listener({ message: \`Unexpected data received \${event.data}\` })); + } + } catch (e: any) { + this.errorListeners.forEach(({ listener }) => listener({ message: \`Failed to parse received data \${event.data}\` })); + } + }; + + private _onError = async (error: WebSocket.ErrorEvent) => { + this.errorListeners.forEach(({ listener }) => listener({ message: error.message })); + }; + + private _sendOrQueueMessage = (route: string, payload?: any): Promise => { + const message = JSON.stringify({ route, payload }); + if (this.status === SocketStatus.CONNECTED) { + this._send(message); + return Promise.resolve(); + } else if (this.status === SocketStatus.DISCONNECTED) { + return Promise.reject(new Error("The socket is not connected. Please call $connect before sending messages")); + } + // Status is CONNECTING, queue the message + return new Promise((resolve, reject) => { + this.messageQueue.push({ message, resolve, reject }); + }); + }; + + private _flushMessageQueue = () => { + while (this.messageQueue.length > 0) { + const { message, resolve } = this.messageQueue.shift(); + this._send(message); + resolve(); + } + }; + + private _rejectMessageQueue = () => { + while (this.messageQueue.length > 0) { + const { reject } = this.messageQueue.shift(); + reject(); + } + }; + + private _send = (message: string) => { + this.socket.send(message); + }; + + private _connect = async (): Promise => { + this.status = SocketStatus.CONNECTING; + let url = this.options.url; + if (this.options.authentication && 'iam' in this.options.authentication && this.options.authentication.iam) { + url = await this._signConnectionUrl(this.options.authentication.iam); + } else if (this.options.authentication && 'custom' in this.options.authentication && this.options.authentication.custom) { + url = (await this.options.authentication.custom.apply({ url })).url; + } + + // Create the socket and wait for it to open (or immediately close) + this.socket = new WebSocket(url); + await (() => { + return new Promise((resolve, reject) => { + this.socket.onopen = () => { + resolve(); + }; + this.socket.onclose = (event) => { + // WebSocket closed immediately + reject(event); + }; + }); + })(); + this.socket.onmessage = this._onMessage; + this.socket.onerror = this._onError; + this.socket.onclose = this._onClose; + this._flushMessageQueue(); + this.status = SocketStatus.CONNECTED; + this.lastConnected = Date.now(); + }; + + /** + * Establish a connection to the server + */ + public $connect = async (): Promise => { + this.connectionAttempt = 0; + await this._connect(); + }; + + private _onDisconnect = () => { + this.status = SocketStatus.DISCONNECTED; + this._rejectMessageQueue(); + }; + + /** + * Disconnect from the server. You must explicitly call $connect to re-establish the connection + */ + public $disconnect = async () => { + if (this.socket) { + this._onDisconnect(); + await (() => new Promise(resolve => { + this.socket.onclose = resolve; + this.socket.close(); + }))(); + this.socket.onclose = null; + this.socket.onmessage = null; + this.socket.onerror = null; + } + }; + + /** + * Register a callback to be called whenever an error occurs. + * @returns a function which will remove the listener when called. + */ + public $onError = (listener: (err: WebSocketError) => void) => { + const listenerId = uuid(); + this.errorListeners.push({ + id: listenerId, + listener, + }); + return () => { + this.errorListeners = this.errorListeners.filter(({ id }) => id !== listenerId); + }; + }; + + /** + * Register a callback to be called whenever any message is received. + * Not recommended for use as this is not type-safe, prefer the "onXXXX" methods to listen to specific routes. + */ + public $onAnyMessage = (listener: (route: string, payload?: any) => void) => { + const listenerId = uuid(); + this.allMessageListeners.push({ + id: listenerId, + listener, + }); + return () => { + this.allMessageListeners = this.allMessageListeners.filter(({ id }) => id !== listenerId); + }; + }; + + /** + * Call the given function immediately, as well as registering it to be invoked whenever the + * websocket reconnects, for example due to a connection timeout. + * @returns a function which will deregister the listener from further calls on reconnect + */ + public $withReconnect = (listener: () => void): () => void => { + const listenerId = uuid(); + this.reconnectListeners.push({ + id: listenerId, + listener, + }); + listener(); + return () => { + this.reconnectListeners = this.reconnectListeners.filter(({ id }) => id !== listenerId); + }; + }; + + /** + * Send a "JavaOne" message to the server + */ + public javaOne = async (input: Request): Promise => { + await this._sendOrQueueMessage("JavaOne", input); + }; + + /** + * Register a listener to be called whenever a "JavaOne" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onJavaOne = (callback: (input: Request) => void): () => void => { + return this._listen("JavaOne", callback); + }; + + /** + * Send a "JavaTwo" message to the server + */ + public javaTwo = async (input: Request): Promise => { + await this._sendOrQueueMessage("JavaTwo", input); + }; + + /** + * Register a listener to be called whenever a "JavaTwo" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onJavaTwo = (callback: (input: Request) => void): () => void => { + return this._listen("JavaTwo", callback); + }; + + /** + * Send a "PythonOne" message to the server + */ + public pythonOne = async (input: Request): Promise => { + await this._sendOrQueueMessage("PythonOne", input); + }; + + /** + * Register a listener to be called whenever a "PythonOne" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onPythonOne = (callback: (input: Request) => void): () => void => { + return this._listen("PythonOne", callback); + }; + + /** + * Send a "PythonTwo" message to the server + */ + public pythonTwo = async (input: Request): Promise => { + await this._sendOrQueueMessage("PythonTwo", input); + }; + + /** + * Register a listener to be called whenever a "PythonTwo" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onPythonTwo = (callback: (input: Request) => void): () => void => { + return this._listen("PythonTwo", callback); + }; + + /** + * Send a "TypescriptOne" message to the server + */ + public typescriptOne = async (input: Request): Promise => { + await this._sendOrQueueMessage("TypescriptOne", input); + }; + + /** + * Register a listener to be called whenever a "TypescriptOne" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onTypescriptOne = (callback: (input: Request) => void): () => void => { + return this._listen("TypescriptOne", callback); + }; + + /** + * Send a "TypescriptTwo" message to the server + */ + public typescriptTwo = async (input: Request): Promise => { + await this._sendOrQueueMessage("TypescriptTwo", input); + }; + + /** + * Register a listener to be called whenever a "TypescriptTwo" message is received from the server + * @returns a function which will remove the listener when called. + */ + public onTypescriptTwo = (callback: (input: Request) => void): () => void => { + return this._listen("TypescriptTwo", callback); + }; + +} +", + "src/index.ts": "/* tslint:disable */ +/* eslint-disable */ +export * from './models/index.js'; +export * from './client/client.js'; +", +} +`; diff --git a/packages/type-safe-api/test/scripts/generators/typescript-esm.test.ts b/packages/type-safe-api/test/scripts/generators/typescript-esm.test.ts new file mode 100644 index 000000000..4ffffa940 --- /dev/null +++ b/packages/type-safe-api/test/scripts/generators/typescript-esm.test.ts @@ -0,0 +1,58 @@ +/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 */ +import os from "os"; +import * as path from "path"; +import { exec } from "projen/lib/util"; +import { withTmpDirSnapshot } from "../../project/snapshot-utils"; + +describe("TypeScript ESM Generator Tests", () => { + it.each([ + ["typescript", "handlers.yaml"], + ["typescript-async-runtime", "async/handlers.yaml"], + ["typescript-cdk-infrastructure", "handlers.yaml"], + ["typescript-async-cdk-infrastructure", "async/handlers.yaml"], + ["typescript-lambda-handlers", "handlers.yaml"], + ["typescript-async-lambda-handlers", "async/handlers.yaml"], + ["typescript-websocket-client", "async/handlers.yaml"], + ])("Generates %s with ESM compatible syntax", (templateDir, spec) => { + const specPath = path.resolve(__dirname, `../../resources/specs/${spec}`); + expect( + withTmpDirSnapshot( + os.tmpdir(), + (outdir) => { + exec(`cp ${specPath} ${outdir}/spec.yaml`, { + cwd: path.resolve(__dirname), + }); + exec( + `${path.resolve( + __dirname, + "../../../scripts/type-safe-api/run.js generate" + )} --templateDirs "${templateDir}" --specPath spec.yaml --outputPath ${outdir} --metadata '${JSON.stringify( + { + esm: true, + srcDir: "src", + tstDir: "test", + runtimePackageName: "@test/runtime", + relativeSpecPath: "spec.yaml", + "x-handlers-python-module": "test_py", + "x-handlers-java-package": "com.java.test", + "x-handlers-typescript-asset-path": "test/ts/dist", + "x-handlers-python-asset-path": "test/py/dist", + "x-handlers-java-asset-path": "test/java/dist", + "x-handlers-node-lambda-runtime-version": "NODEJS_20_X", + "x-handlers-python-lambda-runtime-version": "PYTHON_3_12", + "x-handlers-java-lambda-runtime-version": "JAVA_21", + } + )}'`, + { + cwd: outdir, + } + ); + }, + { + excludeGlobs: ["spec.yaml"], + } + ) + ).toMatchSnapshot(); + }); +});