diff --git a/packages/cloudscape-react-ts-website/src/cloudscape-react-ts-website-project.ts b/packages/cloudscape-react-ts-website/src/cloudscape-react-ts-website-project.ts index cc90faedf..d6fb68d5b 100644 --- a/packages/cloudscape-react-ts-website/src/cloudscape-react-ts-website-project.ts +++ b/packages/cloudscape-react-ts-website/src/cloudscape-react-ts-website-project.ts @@ -132,12 +132,12 @@ export class CloudscapeReactTsWebsiteProject extends ReactTypeScriptProject { this.addDevDeps("@types/swagger-ui-react"); this.addDeps("swagger-ui-react", "aws4fetch"); - tsApi.model.postCompileTask.exec( - `cp .api.json ${path.relative( - tsApi.model.outdir, - this.outdir - )}/public/api.json` - ); + const targetApiSpecPath = `${path.relative( + tsApi.model.outdir, + this.outdir + )}/public/api.json`; + tsApi.model.postCompileTask.exec(`rm -f ${targetApiSpecPath}`); + tsApi.model.postCompileTask.exec(`cp .api.json ${targetApiSpecPath}`); } private buildSampleDirEntries( diff --git a/packages/type-safe-api/scripts/generators/typescript/templates/interceptors.mustache b/packages/type-safe-api/scripts/generators/typescript/templates/interceptors.mustache index 86409aac2..c7528c6f8 100644 --- a/packages/type-safe-api/scripts/generators/typescript/templates/interceptors.mustache +++ b/packages/type-safe-api/scripts/generators/typescript/templates/interceptors.mustache @@ -21,13 +21,11 @@ export const buildTryCatchInterceptor = async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse, >( request: ChainedRequestInput< RequestParameters, - RequestArrayParameters, RequestBody, Response >, @@ -77,22 +75,28 @@ const DEFAULT_CORS_HEADERS: { [key: string]: string } = { * Create an interceptor for adding headers to the response * @param additionalHeaders headers to add to the response */ -export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => async < - RequestParameters, - RequestArrayParameters, - RequestBody, - Response extends OperationResponse ->( - request: ChainedRequestInput, -): Promise => { - const result = await request.chain.next(request); - return { - ...result, - headers: { - ...additionalHeaders, - ...result.headers, - }, +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; }; /** @@ -124,7 +128,7 @@ export class LoggingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { logger.addContext(request.context); logger.appendKeys({ operationId: request.interceptorContext.operationId }); @@ -142,7 +146,7 @@ export class LoggingInterceptor { RequestArrayParameters, RequestBody, Response extends OperationResponse - >(request: ChainedRequestInput): Logger => { + >(request: ChainedRequestInput): Logger => { if (!request.interceptorContext.logger) { throw new Error('No logger found, did you configure the LoggingInterceptor?'); } @@ -176,11 +180,10 @@ export interface TracingInterceptorOptions { */ export const buildTracingInterceptor = (options?: TracingInterceptorOptions) => async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler'; const segment = tracer.getSegment(); @@ -233,7 +236,7 @@ export class TracingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Tracer => { if (!request.interceptorContext.tracer) { throw new Error('No tracer found, did you configure the TracingInterceptor?'); @@ -265,7 +268,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { metrics.addDimension("operationId", request.interceptorContext.operationId); request.interceptorContext.metrics = metrics; @@ -286,7 +289,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Metrics => { if (!request.interceptorContext.metrics) { throw new Error('No metrics logger found, did you configure the MetricsInterceptor?'); diff --git a/packages/type-safe-api/scripts/generators/typescript/templates/operationConfig.mustache b/packages/type-safe-api/scripts/generators/typescript/templates/operationConfig.mustache index 5ac6833bc..8cf452a75 100644 --- a/packages/type-safe-api/scripts/generators/typescript/templates/operationConfig.mustache +++ b/packages/type-safe-api/scripts/generators/typescript/templates/operationConfig.mustache @@ -90,6 +90,86 @@ const decodeRequestParameters = (parameters: ApiGatewayRequestParameters): ApiGa */ 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 }); +}; + type OperationIds ={{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} | '{{nickname}}'{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}; type OperationApiGatewayProxyResult = APIGatewayProxyResult & { __operationId?: T }; @@ -104,50 +184,49 @@ export interface OperationResponse { } // Input for a lambda handler for an operation -export type LambdaRequestParameters = { +export type LambdaRequestParameters = { requestParameters: RequestParameters, - requestArrayParameters: RequestArrayParameters, body: RequestBody, }; export type InterceptorContext = { [key: string]: any }; -export interface RequestInput { - input: LambdaRequestParameters; +export interface RequestInput { + input: LambdaRequestParameters; event: APIGatewayProxyEvent; context: Context; interceptorContext: InterceptorContext; } -export interface ChainedRequestInput extends RequestInput { - chain: LambdaHandlerChain; +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, +export type ChainedLambdaHandlerFunction = ( + input: ChainedRequestInput, ) => Promise; // Type for a lambda handler function to be wrapped -export type LambdaHandlerFunction = ( - input: RequestInput, +export type LambdaHandlerFunction = ( + input: RequestInput, ) => Promise; -export interface LambdaHandlerChain { - next: LambdaHandlerFunction; +export interface LambdaHandlerChain { + next: LambdaHandlerFunction; } // Interceptor is a type alias for ChainedLambdaHandlerFunction -export type Interceptor = ChainedLambdaHandlerFunction; +export type Interceptor = ChainedLambdaHandlerFunction; /** * Build a chain from the given array of chained lambda handlers */ -const buildHandlerChain = ( - ...handlers: ChainedLambdaHandlerFunction[] -): LambdaHandlerChain => { +const buildHandlerChain = ( + ...handlers: ChainedLambdaHandlerFunction[] +): LambdaHandlerChain => { if (handlers.length === 0) { return { next: () => { @@ -171,27 +250,12 @@ const buildHandlerChain = ; -export type {{operationIdCamelCase}}ChainedHandlerFunction = ChainedLambdaHandlerFunction<{{operationIdCamelCase}}RequestParameters, {{operationIdCamelCase}}RequestArrayParameters, {{operationIdCamelCase}}RequestBody, {{operationIdCamelCase}}OperationResponses>; +export type {{operationIdCamelCase}}HandlerFunction = LambdaHandlerFunction<{{operationIdCamelCase}}RequestParameters, {{operationIdCamelCase}}RequestBody, {{operationIdCamelCase}}OperationResponses>; +export type {{operationIdCamelCase}}ChainedHandlerFunction = ChainedLambdaHandlerFunction<{{operationIdCamelCase}}RequestParameters, {{operationIdCamelCase}}RequestBody, {{operationIdCamelCase}}OperationResponses>; +export type {{operationIdCamelCase}}ChainedRequestInput = ChainedRequestInput<{{operationIdCamelCase}}RequestParameters, {{operationIdCamelCase}}RequestBody, {{operationIdCamelCase}}OperationResponses>; /** * Lambda handler wrapper to provide typed interface for the implementation of {{nickname}} @@ -217,43 +282,16 @@ export const {{nickname}}Handler = ( ...handlers: [{{operationIdCamelCase}}ChainedHandlerFunction, ...{{operationIdCamelCase}}ChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'{{nickname}}'> => async (event: any, context: any, _callback?: any, additionalInterceptors: {{operationIdCamelCase}}ChainedHandlerFunction[] = []): Promise => { const operationId = "{{nickname}}"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as {{operationIdCamelCase}}RequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as {{operationIdCamelCase}}RequestArrayParameters; - - const demarshal = (bodyString: string): any => { - {{#bodyParam}} - {{^isPrimitiveType}} - return {{dataType}}FromJSON(JSON.parse(bodyString)); - {{/isPrimitiveType}} - {{#isPrimitiveType}} - return bodyString; - {{/isPrimitiveType}} - {{/bodyParam}} - {{^bodyParam}} - return {}; - {{/bodyParam}} - }; - const body = parseBody(event.body, demarshal, [{{^consumes}}'application/json'{{/consumes}}{{#consumes}}{{#mediaType}}'{{{.}}}',{{/mediaType}}{{/consumes}}]) as {{operationIdCamelCase}}RequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -293,6 +331,58 @@ export const {{nickname}}Handler = ( return headers; }; + let requestParameters: {{operationIdCamelCase}}RequestParameters | undefined = undefined; + + try { + requestParameters = { + {{#allParams}} + {{^isBodyParam}} + {{paramName}}: coerceParameter("{{baseName}}", "{{{dataType}}}", {{#isArray}}{{#items}}{{isInteger}} || {{isLong}} || {{isShort}}{{/items}}{{/isArray}}{{^isArray}}{{isInteger}} || {{isLong}} || {{isShort}}{{/isArray}}, rawSingleValueParameters, rawMultiValueParameters, {{required}}) as {{{dataType}}}{{^required}} | undefined{{/required}}, + {{/isBodyParam}} + {{/allParams}} + }; + } 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 => { + {{#bodyParam}} + {{^isPrimitiveType}} + return {{dataType}}FromJSON(JSON.parse(bodyString)); + {{/isPrimitiveType}} + {{#isPrimitiveType}} + return bodyString; + {{/isPrimitiveType}} + {{/bodyParam}} + {{^bodyParam}} + return {}; + {{/bodyParam}} + }; + const body = parseBody(event.body, demarshal, [{{^consumes}}'application/json'{{/consumes}}{{#consumes}}{{#mediaType}}'{{{.}}}',{{/mediaType}}{{/consumes}}]) as {{operationIdCamelCase}}RequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -320,13 +410,11 @@ export interface HandlerRouterHandlers { } export type AnyOperationRequestParameters = {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestParameters{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}; -export type AnyOperationRequestArrayParameters = {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestArrayParameters{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}; export type AnyOperationRequestBodies = {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestBody{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}; export type AnyOperationResponses = {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| {{operationIdCamelCase}}OperationResponses{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}; export interface HandlerRouterProps< RequestParameters, - RequestArrayParameters, RequestBody, Response extends AnyOperationResponses > { @@ -335,7 +423,6 @@ export interface HandlerRouterProps< */ readonly interceptors?: ChainedLambdaHandlerFunction< RequestParameters, - RequestArrayParameters, RequestBody, Response >[]; @@ -357,7 +444,6 @@ const OperationIdByMethodAndPath = Object.fromEntries(Object.entries(OperationLo */ export const handlerRouter = (props: HandlerRouterProps< AnyOperationRequestParameters, - AnyOperationRequestArrayParameters, AnyOperationRequestBodies, AnyOperationResponses >): OperationApiGatewayLambdaHandler => async (event, context) => { diff --git a/packages/type-safe-api/src/project/model/smithy/smithy-definition.ts b/packages/type-safe-api/src/project/model/smithy/smithy-definition.ts index 721ae19e4..31bfa3769 100644 --- a/packages/type-safe-api/src/project/model/smithy/smithy-definition.ts +++ b/packages/type-safe-api/src/project/model/smithy/smithy-definition.ts @@ -223,6 +223,9 @@ structure NotAuthorizedError { service: `${serviceNamespace}#${serviceName}`, // By default, preserve tags in the generated spec, but allow users to explicitly overwrite this tags: true, + // By default, use integer types as this is more intuitive when smithy distinguishes between Integers and Doubles. + // Users may also override this. + useIntegerType: true, ...smithyOptions.smithyBuildOptions?.projections?.openapi?.plugins ?.openapi, }, diff --git a/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap b/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap index 4b6fb1155..86e423da2 100644 --- a/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap +++ b/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap @@ -27066,6 +27066,7 @@ rootProject.name = 'smithy-handlers-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -29575,6 +29576,7 @@ rootProject.name = 'smithy-typescript-react-query-hooks-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -32563,6 +32565,7 @@ rootProject.name = 'smithy-java-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -37005,6 +37008,7 @@ rootProject.name = 'smithy-java-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -39448,6 +39452,7 @@ rootProject.name = 'smithy-npm-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -43046,6 +43051,7 @@ rootProject.name = 'smithy-npm-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -45494,6 +45500,7 @@ rootProject.name = 'smithy-pnpm-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -49104,6 +49111,7 @@ rootProject.name = 'smithy-pnpm-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -52135,6 +52143,7 @@ rootProject.name = 'smithy-python-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -56484,6 +56493,7 @@ rootProject.name = 'smithy-python-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -59807,6 +59817,7 @@ rootProject.name = 'smithy-typescript-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -64445,6 +64456,7 @@ rootProject.name = 'smithy-typescript-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -66888,6 +66900,7 @@ rootProject.name = 'smithy-yarn-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -70487,6 +70500,7 @@ rootProject.name = 'smithy-yarn-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -72930,6 +72944,7 @@ rootProject.name = 'smithy-yarn2-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -76533,6 +76548,7 @@ rootProject.name = 'smithy-yarn2-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, diff --git a/packages/type-safe-api/test/project/model/__snapshots__/type-safe-api-model-project.test.ts.snap b/packages/type-safe-api/test/project/model/__snapshots__/type-safe-api-model-project.test.ts.snap index 06dec1660..18891f679 100644 --- a/packages/type-safe-api/test/project/model/__snapshots__/type-safe-api-model-project.test.ts.snap +++ b/packages/type-safe-api/test/project/model/__snapshots__/type-safe-api-model-project.test.ts.snap @@ -965,6 +965,7 @@ rootProject.name = 'smithy-model' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -1379,6 +1380,7 @@ rootProject.name = 'smithy-model-with-build-options' "ignoreUnsupportedTraits": true, "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, @@ -1796,6 +1798,7 @@ rootProject.name = 'smithy-model-consumer' "openapi": { "service": "com.test#Consumer", "tags": true, + "useIntegerType": true, }, }, }, @@ -2210,6 +2213,7 @@ rootProject.name = 'smithy-handlers' "openapi": { "service": "com.test#MyService", "tags": true, + "useIntegerType": true, }, }, }, diff --git a/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript.test.ts.snap b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript.test.ts.snap index ca28765bd..f0bacf6b4 100644 --- a/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript.test.ts.snap +++ b/packages/type-safe-api/test/scripts/generators/__snapshots__/typescript.test.ts.snap @@ -236,6 +236,86 @@ const decodeRequestParameters = (parameters: ApiGatewayRequestParameters): ApiGa */ 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 }); +}; + type OperationIds = | 'neither' | 'both' | 'tag1' | 'tag2'; type OperationApiGatewayProxyResult = APIGatewayProxyResult & { __operationId?: T }; @@ -250,50 +330,49 @@ export interface OperationResponse { } // Input for a lambda handler for an operation -export type LambdaRequestParameters = { +export type LambdaRequestParameters = { requestParameters: RequestParameters, - requestArrayParameters: RequestArrayParameters, body: RequestBody, }; export type InterceptorContext = { [key: string]: any }; -export interface RequestInput { - input: LambdaRequestParameters; +export interface RequestInput { + input: LambdaRequestParameters; event: APIGatewayProxyEvent; context: Context; interceptorContext: InterceptorContext; } -export interface ChainedRequestInput extends RequestInput { - chain: LambdaHandlerChain; +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, +export type ChainedLambdaHandlerFunction = ( + input: ChainedRequestInput, ) => Promise; // Type for a lambda handler function to be wrapped -export type LambdaHandlerFunction = ( - input: RequestInput, +export type LambdaHandlerFunction = ( + input: RequestInput, ) => Promise; -export interface LambdaHandlerChain { - next: LambdaHandlerFunction; +export interface LambdaHandlerChain { + next: LambdaHandlerFunction; } // Interceptor is a type alias for ChainedLambdaHandlerFunction -export type Interceptor = ChainedLambdaHandlerFunction; +export type Interceptor = ChainedLambdaHandlerFunction; /** * Build a chain from the given array of chained lambda handlers */ -const buildHandlerChain = ( - ...handlers: ChainedLambdaHandlerFunction[] -): LambdaHandlerChain => { +const buildHandlerChain = ( + ...handlers: ChainedLambdaHandlerFunction[] +): LambdaHandlerChain => { if (handlers.length === 0) { return { next: () => { @@ -313,17 +392,11 @@ const buildHandlerChain = ; export type NeitherOperationResponses = | Neither200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type NeitherHandlerFunction = LambdaHandlerFunction; -export type NeitherChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type NeitherHandlerFunction = LambdaHandlerFunction; +export type NeitherChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type NeitherChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of neither @@ -343,33 +417,16 @@ export const neitherHandler = ( ...handlers: [NeitherChainedHandlerFunction, ...NeitherChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'neither'> => async (event: any, context: any, _callback?: any, additionalInterceptors: NeitherChainedHandlerFunction[] = []): Promise => { const operationId = "neither"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as NeitherRequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as NeitherRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as NeitherRequestBody; - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -394,6 +451,43 @@ export const neitherHandler = ( return headers; }; + let requestParameters: NeitherRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 NeitherRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -404,17 +498,11 @@ export const neitherHandler = ( }; }; /** - * Single-value path/query/header parameters for Both + * Path, Query and Header parameters for Both */ export interface BothRequestParameters { } -/** - * Multi-value query or header parameters for Both - */ -export interface BothRequestArrayParameters { -} - /** * Request body parameter for Both */ @@ -424,8 +512,9 @@ export type Both200OperationResponse = OperationResponse<200, undefined>; export type BothOperationResponses = | Both200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type BothHandlerFunction = LambdaHandlerFunction; -export type BothChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type BothHandlerFunction = LambdaHandlerFunction; +export type BothChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type BothChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of both @@ -434,33 +523,16 @@ export const bothHandler = ( ...handlers: [BothChainedHandlerFunction, ...BothChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'both'> => async (event: any, context: any, _callback?: any, additionalInterceptors: BothChainedHandlerFunction[] = []): Promise => { const operationId = "both"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as BothRequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as BothRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as BothRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -485,6 +557,43 @@ export const bothHandler = ( return headers; }; + let requestParameters: BothRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 BothRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -495,17 +604,11 @@ export const bothHandler = ( }; }; /** - * Single-value path/query/header parameters for Tag1 + * Path, Query and Header parameters for Tag1 */ export interface Tag1RequestParameters { } -/** - * Multi-value query or header parameters for Tag1 - */ -export interface Tag1RequestArrayParameters { -} - /** * Request body parameter for Tag1 */ @@ -515,8 +618,9 @@ export type Tag1200OperationResponse = OperationResponse<200, undefined>; export type Tag1OperationResponses = | Tag1200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type Tag1HandlerFunction = LambdaHandlerFunction; -export type Tag1ChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type Tag1HandlerFunction = LambdaHandlerFunction; +export type Tag1ChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type Tag1ChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of tag1 @@ -525,33 +629,16 @@ export const tag1Handler = ( ...handlers: [Tag1ChainedHandlerFunction, ...Tag1ChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'tag1'> => async (event: any, context: any, _callback?: any, additionalInterceptors: Tag1ChainedHandlerFunction[] = []): Promise => { const operationId = "tag1"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as Tag1RequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as Tag1RequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as Tag1RequestBody; - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -576,6 +663,43 @@ export const tag1Handler = ( return headers; }; + let requestParameters: Tag1RequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 Tag1RequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -586,17 +710,11 @@ export const tag1Handler = ( }; }; /** - * Single-value path/query/header parameters for Tag2 + * Path, Query and Header parameters for Tag2 */ export interface Tag2RequestParameters { } -/** - * Multi-value query or header parameters for Tag2 - */ -export interface Tag2RequestArrayParameters { -} - /** * Request body parameter for Tag2 */ @@ -606,8 +724,9 @@ export type Tag2200OperationResponse = OperationResponse<200, undefined>; export type Tag2OperationResponses = | Tag2200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type Tag2HandlerFunction = LambdaHandlerFunction; -export type Tag2ChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type Tag2HandlerFunction = LambdaHandlerFunction; +export type Tag2ChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type Tag2ChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of tag2 @@ -616,33 +735,16 @@ export const tag2Handler = ( ...handlers: [Tag2ChainedHandlerFunction, ...Tag2ChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'tag2'> => async (event: any, context: any, _callback?: any, additionalInterceptors: Tag2ChainedHandlerFunction[] = []): Promise => { const operationId = "tag2"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as Tag2RequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as Tag2RequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as Tag2RequestBody; - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -667,6 +769,43 @@ export const tag2Handler = ( return headers; }; + let requestParameters: Tag2RequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 Tag2RequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -685,13 +824,11 @@ export interface HandlerRouterHandlers { } export type AnyOperationRequestParameters = | NeitherRequestParameters| BothRequestParameters| Tag1RequestParameters| Tag2RequestParameters; -export type AnyOperationRequestArrayParameters = | NeitherRequestArrayParameters| BothRequestArrayParameters| Tag1RequestArrayParameters| Tag2RequestArrayParameters; export type AnyOperationRequestBodies = | NeitherRequestBody| BothRequestBody| Tag1RequestBody| Tag2RequestBody; export type AnyOperationResponses = | NeitherOperationResponses| BothOperationResponses| Tag1OperationResponses| Tag2OperationResponses; export interface HandlerRouterProps< RequestParameters, - RequestArrayParameters, RequestBody, Response extends AnyOperationResponses > { @@ -700,7 +837,6 @@ export interface HandlerRouterProps< */ readonly interceptors?: ChainedLambdaHandlerFunction< RequestParameters, - RequestArrayParameters, RequestBody, Response >[]; @@ -722,7 +858,6 @@ const OperationIdByMethodAndPath = Object.fromEntries(Object.entries(OperationLo */ export const handlerRouter = (props: HandlerRouterProps< AnyOperationRequestParameters, - AnyOperationRequestArrayParameters, AnyOperationRequestBodies, AnyOperationResponses >): OperationApiGatewayLambdaHandler => async (event, context) => { @@ -875,22 +1010,28 @@ const DEFAULT_CORS_HEADERS: { [key: string]: string } = { * Create an interceptor for adding headers to the response * @param additionalHeaders headers to add to the response */ -export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => async < - RequestParameters, - RequestArrayParameters, - RequestBody, - Response extends OperationResponse ->( - request: ChainedRequestInput, -): Promise => { - const result = await request.chain.next(request); - return { - ...result, - headers: { - ...additionalHeaders, - ...result.headers, - }, +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; }; /** @@ -939,7 +1080,7 @@ export class LoggingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { logger.addContext(request.context); logger.appendKeys({ operationId: request.interceptorContext.operationId }); @@ -957,7 +1098,7 @@ export class LoggingInterceptor { RequestArrayParameters, RequestBody, Response extends OperationResponse - >(request: ChainedRequestInput): Logger => { + >(request: ChainedRequestInput): Logger => { if (!request.interceptorContext.logger) { throw new Error('No logger found, did you configure the LoggingInterceptor?'); } @@ -982,7 +1123,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { metrics.addDimension("operationId", request.interceptorContext.operationId); request.interceptorContext.metrics = metrics; @@ -1003,7 +1144,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Metrics => { if (!request.interceptorContext.metrics) { throw new Error('No metrics logger found, did you configure the MetricsInterceptor?'); @@ -1032,11 +1173,10 @@ export interface TracingInterceptorOptions { */ export const buildTracingInterceptor = (options?: TracingInterceptorOptions) => async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler'; const segment = tracer.getSegment(); @@ -1089,7 +1229,7 @@ export class TracingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Tracer => { if (!request.interceptorContext.tracer) { throw new Error('No tracer found, did you configure the TracingInterceptor?'); @@ -1113,13 +1253,11 @@ export const buildTryCatchInterceptor = async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse, >( request: ChainedRequestInput< RequestParameters, - RequestArrayParameters, RequestBody, Response >, @@ -2163,6 +2301,86 @@ const decodeRequestParameters = (parameters: ApiGatewayRequestParameters): ApiGa */ 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 }); +}; + type OperationIds = | 'anyRequestResponse' | 'empty' | 'mapResponse' | 'mediaTypes' | 'multipleContentTypes' | 'operationOne' | 'withoutOperationIdDelete'; type OperationApiGatewayProxyResult = APIGatewayProxyResult & { __operationId?: T }; @@ -2177,50 +2395,49 @@ export interface OperationResponse { } // Input for a lambda handler for an operation -export type LambdaRequestParameters = { +export type LambdaRequestParameters = { requestParameters: RequestParameters, - requestArrayParameters: RequestArrayParameters, body: RequestBody, }; export type InterceptorContext = { [key: string]: any }; -export interface RequestInput { - input: LambdaRequestParameters; +export interface RequestInput { + input: LambdaRequestParameters; event: APIGatewayProxyEvent; context: Context; interceptorContext: InterceptorContext; } -export interface ChainedRequestInput extends RequestInput { - chain: LambdaHandlerChain; +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, +export type ChainedLambdaHandlerFunction = ( + input: ChainedRequestInput, ) => Promise; // Type for a lambda handler function to be wrapped -export type LambdaHandlerFunction = ( - input: RequestInput, +export type LambdaHandlerFunction = ( + input: RequestInput, ) => Promise; -export interface LambdaHandlerChain { - next: LambdaHandlerFunction; +export interface LambdaHandlerChain { + next: LambdaHandlerFunction; } // Interceptor is a type alias for ChainedLambdaHandlerFunction -export type Interceptor = ChainedLambdaHandlerFunction; +export type Interceptor = ChainedLambdaHandlerFunction; /** * Build a chain from the given array of chained lambda handlers */ -const buildHandlerChain = ( - ...handlers: ChainedLambdaHandlerFunction[] -): LambdaHandlerChain => { +const buildHandlerChain = ( + ...handlers: ChainedLambdaHandlerFunction[] +): LambdaHandlerChain => { if (handlers.length === 0) { return { next: () => { @@ -2240,17 +2457,11 @@ const buildHandlerChain = ; -export type AnyRequestResponseChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type AnyRequestResponseHandlerFunction = LambdaHandlerFunction; +export type AnyRequestResponseChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type AnyRequestResponseChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of anyRequestResponse @@ -2270,33 +2482,16 @@ export const anyRequestResponseHandler = ( ...handlers: [AnyRequestResponseChainedHandlerFunction, ...AnyRequestResponseChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'anyRequestResponse'> => async (event: any, context: any, _callback?: any, additionalInterceptors: AnyRequestResponseChainedHandlerFunction[] = []): Promise => { const operationId = "anyRequestResponse"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as AnyRequestResponseRequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as AnyRequestResponseRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return bodyString; - }; - const body = parseBody(event.body, demarshal, ['application/json',]) as AnyRequestResponseRequestBody; - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2321,25 +2516,56 @@ export const anyRequestResponseHandler = ( return headers; }; - return { - ...response, + let requestParameters: AnyRequestResponseRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } catch (e: any) { + const res = { + statusCode: 400, + body: { message: e.message }, + headers: extractResponseHeadersFromInterceptors(handlers), + }; + return { + ...res, headers: { - ...errorHeaders(response.statusCode), - ...response.headers, + ...errorHeaders(res.statusCode), + ...res.headers, }, - body: response.body ? marshal(response.statusCode, response.body) : '', - }; -}; -/** - * Single-value path/query/header parameters for Empty - */ -export interface EmptyRequestParameters { -} + body: res.body ? marshal(res.statusCode, res.body) : '', + }; + } + const demarshal = (bodyString: string): any => { + return bodyString; + }; + const body = parseBody(event.body, demarshal, ['application/json',]) as AnyRequestResponseRequestBody; + + 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) : '', + }; +}; /** - * Multi-value query or header parameters for Empty + * Path, Query and Header parameters for Empty */ -export interface EmptyRequestArrayParameters { +export interface EmptyRequestParameters { } /** @@ -2351,8 +2577,9 @@ export type Empty204OperationResponse = OperationResponse<204, undefined>; export type EmptyOperationResponses = | Empty204OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type EmptyHandlerFunction = LambdaHandlerFunction; -export type EmptyChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type EmptyHandlerFunction = LambdaHandlerFunction; +export type EmptyChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type EmptyChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of empty @@ -2361,33 +2588,16 @@ export const emptyHandler = ( ...handlers: [EmptyChainedHandlerFunction, ...EmptyChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'empty'> => async (event: any, context: any, _callback?: any, additionalInterceptors: EmptyChainedHandlerFunction[] = []): Promise => { const operationId = "empty"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as EmptyRequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as EmptyRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as EmptyRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2412,6 +2622,43 @@ export const emptyHandler = ( return headers; }; + let requestParameters: EmptyRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 EmptyRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2422,17 +2669,11 @@ export const emptyHandler = ( }; }; /** - * Single-value path/query/header parameters for MapResponse + * Path, Query and Header parameters for MapResponse */ export interface MapResponseRequestParameters { } -/** - * Multi-value query or header parameters for MapResponse - */ -export interface MapResponseRequestArrayParameters { -} - /** * Request body parameter for MapResponse */ @@ -2442,8 +2683,9 @@ export type MapResponse200OperationResponse = OperationResponse<200, MapResponse export type MapResponseOperationResponses = | MapResponse200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type MapResponseHandlerFunction = LambdaHandlerFunction; -export type MapResponseChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MapResponseHandlerFunction = LambdaHandlerFunction; +export type MapResponseChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MapResponseChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of mapResponse @@ -2452,33 +2694,16 @@ export const mapResponseHandler = ( ...handlers: [MapResponseChainedHandlerFunction, ...MapResponseChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'mapResponse'> => async (event: any, context: any, _callback?: any, additionalInterceptors: MapResponseChainedHandlerFunction[] = []): Promise => { const operationId = "mapResponse"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as MapResponseRequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as MapResponseRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as MapResponseRequestBody; - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2504,6 +2729,43 @@ export const mapResponseHandler = ( return headers; }; + let requestParameters: MapResponseRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 MapResponseRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2514,17 +2776,11 @@ export const mapResponseHandler = ( }; }; /** - * Single-value path/query/header parameters for MediaTypes + * Path, Query and Header parameters for MediaTypes */ export interface MediaTypesRequestParameters { } -/** - * Multi-value query or header parameters for MediaTypes - */ -export interface MediaTypesRequestArrayParameters { -} - /** * Request body parameter for MediaTypes */ @@ -2534,8 +2790,9 @@ export type MediaTypes200OperationResponse = OperationResponse<200, string>; export type MediaTypesOperationResponses = | MediaTypes200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type MediaTypesHandlerFunction = LambdaHandlerFunction; -export type MediaTypesChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MediaTypesHandlerFunction = LambdaHandlerFunction; +export type MediaTypesChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MediaTypesChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of mediaTypes @@ -2544,33 +2801,16 @@ export const mediaTypesHandler = ( ...handlers: [MediaTypesChainedHandlerFunction, ...MediaTypesChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'mediaTypes'> => async (event: any, context: any, _callback?: any, additionalInterceptors: MediaTypesChainedHandlerFunction[] = []): Promise => { const operationId = "mediaTypes"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as MediaTypesRequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as MediaTypesRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return bodyString; - }; - const body = parseBody(event.body, demarshal, ['application/pdf',]) as MediaTypesRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2595,6 +2835,43 @@ export const mediaTypesHandler = ( return headers; }; + let requestParameters: MediaTypesRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 bodyString; + }; + const body = parseBody(event.body, demarshal, ['application/pdf',]) as MediaTypesRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2605,17 +2882,11 @@ export const mediaTypesHandler = ( }; }; /** - * Single-value path/query/header parameters for MultipleContentTypes + * Path, Query and Header parameters for MultipleContentTypes */ export interface MultipleContentTypesRequestParameters { } -/** - * Multi-value query or header parameters for MultipleContentTypes - */ -export interface MultipleContentTypesRequestArrayParameters { -} - /** * Request body parameter for MultipleContentTypes */ @@ -2625,8 +2896,9 @@ export type MultipleContentTypes200OperationResponse = OperationResponse<200, st export type MultipleContentTypesOperationResponses = | MultipleContentTypes200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type MultipleContentTypesHandlerFunction = LambdaHandlerFunction; -export type MultipleContentTypesChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MultipleContentTypesHandlerFunction = LambdaHandlerFunction; +export type MultipleContentTypesChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type MultipleContentTypesChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of multipleContentTypes @@ -2635,33 +2907,16 @@ export const multipleContentTypesHandler = ( ...handlers: [MultipleContentTypesChainedHandlerFunction, ...MultipleContentTypesChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'multipleContentTypes'> => async (event: any, context: any, _callback?: any, additionalInterceptors: MultipleContentTypesChainedHandlerFunction[] = []): Promise => { const operationId = "multipleContentTypes"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as MultipleContentTypesRequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as MultipleContentTypesRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return TestRequestFromJSON(JSON.parse(bodyString)); - }; - const body = parseBody(event.body, demarshal, ['application/json','application/pdf',]) as MultipleContentTypesRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2686,6 +2941,43 @@ export const multipleContentTypesHandler = ( return headers; }; + let requestParameters: MultipleContentTypesRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 TestRequestFromJSON(JSON.parse(bodyString)); + }; + const body = parseBody(event.body, demarshal, ['application/json','application/pdf',]) as MultipleContentTypesRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2696,22 +2988,16 @@ export const multipleContentTypesHandler = ( }; }; /** - * Single-value path/query/header parameters for OperationOne + * Path, Query and Header parameters for OperationOne */ export interface OperationOneRequestParameters { readonly param1: string; - readonly param3: string; + readonly param2: Array; + readonly param3: number; readonly pathParam: string; - readonly "x-header-param": string; + readonly xHeaderParam: string; readonly param4?: string; -} - -/** - * Multi-value query or header parameters for OperationOne - */ -export interface OperationOneRequestArrayParameters { - readonly param2: string[]; - readonly "x-multi-value-header-param"?: string[]; + readonly xMultiValueHeaderParam?: Array; } /** @@ -2724,8 +3010,9 @@ export type OperationOne400OperationResponse = OperationResponse<400, ApiError>; export type OperationOneOperationResponses = | OperationOne200OperationResponse | OperationOne400OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type OperationOneHandlerFunction = LambdaHandlerFunction; -export type OperationOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type OperationOneHandlerFunction = LambdaHandlerFunction; +export type OperationOneChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type OperationOneChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of operationOne @@ -2734,33 +3021,16 @@ export const operationOneHandler = ( ...handlers: [OperationOneChainedHandlerFunction, ...OperationOneChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'operationOne'> => async (event: any, context: any, _callback?: any, additionalInterceptors: OperationOneChainedHandlerFunction[] = []): Promise => { const operationId = "operationOne"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as OperationOneRequestParameters; - - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as OperationOneRequestArrayParameters; - const demarshal = (bodyString: string): any => { - return TestRequestFromJSON(JSON.parse(bodyString)); - }; - const body = parseBody(event.body, demarshal, ['application/json',]) as OperationOneRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2795,6 +3065,50 @@ export const operationOneHandler = ( return headers; }; + let requestParameters: OperationOneRequestParameters | undefined = undefined; + + try { + requestParameters = { + param1: coerceParameter("param1", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + param2: coerceParameter("param2", "Array", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as Array, + param3: coerceParameter("param3", "number", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as number, + pathParam: coerceParameter("pathParam", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + xHeaderParam: coerceParameter("x-header-param", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, true) as string, + param4: coerceParameter("param4", "string", false || false || false, rawSingleValueParameters, rawMultiValueParameters, false) as string | undefined, + xMultiValueHeaderParam: coerceParameter("x-multi-value-header-param", "Array", false || false || false, rawSingleValueParameters, rawMultiValueParameters, false) as Array | undefined, + }; + } 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 TestRequestFromJSON(JSON.parse(bodyString)); + }; + const body = parseBody(event.body, demarshal, ['application/json',]) as OperationOneRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2805,17 +3119,11 @@ export const operationOneHandler = ( }; }; /** - * Single-value path/query/header parameters for WithoutOperationIdDelete + * Path, Query and Header parameters for WithoutOperationIdDelete */ export interface WithoutOperationIdDeleteRequestParameters { } -/** - * Multi-value query or header parameters for WithoutOperationIdDelete - */ -export interface WithoutOperationIdDeleteRequestArrayParameters { -} - /** * Request body parameter for WithoutOperationIdDelete */ @@ -2825,8 +3133,9 @@ export type WithoutOperationIdDelete200OperationResponse = OperationResponse<200 export type WithoutOperationIdDeleteOperationResponses = | WithoutOperationIdDelete200OperationResponse ; // Type that the handler function provided to the wrapper must conform to -export type WithoutOperationIdDeleteHandlerFunction = LambdaHandlerFunction; -export type WithoutOperationIdDeleteChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type WithoutOperationIdDeleteHandlerFunction = LambdaHandlerFunction; +export type WithoutOperationIdDeleteChainedHandlerFunction = ChainedLambdaHandlerFunction; +export type WithoutOperationIdDeleteChainedRequestInput = ChainedRequestInput; /** * Lambda handler wrapper to provide typed interface for the implementation of withoutOperationIdDelete @@ -2835,33 +3144,16 @@ export const withoutOperationIdDeleteHandler = ( ...handlers: [WithoutOperationIdDeleteChainedHandlerFunction, ...WithoutOperationIdDeleteChainedHandlerFunction[]] ): OperationApiGatewayLambdaHandler<'withoutOperationIdDelete'> => async (event: any, context: any, _callback?: any, additionalInterceptors: WithoutOperationIdDeleteChainedHandlerFunction[] = []): Promise => { const operationId = "withoutOperationIdDelete"; - const requestParameters = decodeRequestParameters({ - ...(event.pathParameters || {}), - ...(event.queryStringParameters || {}), - ...(event.headers || {}), - }) as unknown as WithoutOperationIdDeleteRequestParameters; - const requestArrayParameters = decodeRequestParameters({ - ...(event.multiValueQueryStringParameters || {}), - ...(event.multiValueHeaders || {}), - }) as unknown as WithoutOperationIdDeleteRequestArrayParameters; - - const demarshal = (bodyString: string): any => { - return {}; - }; - const body = parseBody(event.body, demarshal, ['application/json']) as WithoutOperationIdDeleteRequestBody; - - const chain = buildHandlerChain(...additionalInterceptors, ...handlers); - const response = await chain.next({ - input: { - requestParameters, - requestArrayParameters, - body, - }, - event, - context, - interceptorContext: { operationId }, - }); + 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; @@ -2887,6 +3179,43 @@ export const withoutOperationIdDeleteHandler = ( return headers; }; + let requestParameters: WithoutOperationIdDeleteRequestParameters | undefined = undefined; + + try { + requestParameters = { + }; + } 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 WithoutOperationIdDeleteRequestBody; + + const chain = buildHandlerChain(...additionalInterceptors, ...handlers); + const response = await chain.next({ + input: { + requestParameters, + body, + }, + event, + context, + interceptorContext: { operationId }, + }); + return { ...response, headers: { @@ -2908,13 +3237,11 @@ export interface HandlerRouterHandlers { } export type AnyOperationRequestParameters = | AnyRequestResponseRequestParameters| EmptyRequestParameters| MapResponseRequestParameters| MediaTypesRequestParameters| MultipleContentTypesRequestParameters| OperationOneRequestParameters| WithoutOperationIdDeleteRequestParameters; -export type AnyOperationRequestArrayParameters = | AnyRequestResponseRequestArrayParameters| EmptyRequestArrayParameters| MapResponseRequestArrayParameters| MediaTypesRequestArrayParameters| MultipleContentTypesRequestArrayParameters| OperationOneRequestArrayParameters| WithoutOperationIdDeleteRequestArrayParameters; export type AnyOperationRequestBodies = | AnyRequestResponseRequestBody| EmptyRequestBody| MapResponseRequestBody| MediaTypesRequestBody| MultipleContentTypesRequestBody| OperationOneRequestBody| WithoutOperationIdDeleteRequestBody; export type AnyOperationResponses = | AnyRequestResponseOperationResponses| EmptyOperationResponses| MapResponseOperationResponses| MediaTypesOperationResponses| MultipleContentTypesOperationResponses| OperationOneOperationResponses| WithoutOperationIdDeleteOperationResponses; export interface HandlerRouterProps< RequestParameters, - RequestArrayParameters, RequestBody, Response extends AnyOperationResponses > { @@ -2923,7 +3250,6 @@ export interface HandlerRouterProps< */ readonly interceptors?: ChainedLambdaHandlerFunction< RequestParameters, - RequestArrayParameters, RequestBody, Response >[]; @@ -2945,7 +3271,6 @@ const OperationIdByMethodAndPath = Object.fromEntries(Object.entries(OperationLo */ export const handlerRouter = (props: HandlerRouterProps< AnyOperationRequestParameters, - AnyOperationRequestArrayParameters, AnyOperationRequestBodies, AnyOperationResponses >): OperationApiGatewayLambdaHandler => async (event, context) => { @@ -2979,22 +3304,28 @@ const DEFAULT_CORS_HEADERS: { [key: string]: string } = { * Create an interceptor for adding headers to the response * @param additionalHeaders headers to add to the response */ -export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => async < - RequestParameters, - RequestArrayParameters, - RequestBody, - Response extends OperationResponse ->( - request: ChainedRequestInput, -): Promise => { - const result = await request.chain.next(request); - return { - ...result, - headers: { - ...additionalHeaders, - ...result.headers, - }, +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; }; /** @@ -3043,7 +3374,7 @@ export class LoggingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { logger.addContext(request.context); logger.appendKeys({ operationId: request.interceptorContext.operationId }); @@ -3061,7 +3392,7 @@ export class LoggingInterceptor { RequestArrayParameters, RequestBody, Response extends OperationResponse - >(request: ChainedRequestInput): Logger => { + >(request: ChainedRequestInput): Logger => { if (!request.interceptorContext.logger) { throw new Error('No logger found, did you configure the LoggingInterceptor?'); } @@ -3086,7 +3417,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { metrics.addDimension("operationId", request.interceptorContext.operationId); request.interceptorContext.metrics = metrics; @@ -3107,7 +3438,7 @@ export class MetricsInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Metrics => { if (!request.interceptorContext.metrics) { throw new Error('No metrics logger found, did you configure the MetricsInterceptor?'); @@ -3136,11 +3467,10 @@ export interface TracingInterceptorOptions { */ export const buildTracingInterceptor = (options?: TracingInterceptorOptions) => async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Promise => { const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler'; const segment = tracer.getSegment(); @@ -3193,7 +3523,7 @@ export class TracingInterceptor { RequestBody, Response extends OperationResponse >( - request: ChainedRequestInput, + request: ChainedRequestInput, ): Tracer => { if (!request.interceptorContext.tracer) { throw new Error('No tracer found, did you configure the TracingInterceptor?'); @@ -3217,13 +3547,11 @@ export const buildTryCatchInterceptor = async < RequestParameters, - RequestArrayParameters, RequestBody, Response extends OperationResponse, >( request: ChainedRequestInput< RequestParameters, - RequestArrayParameters, RequestBody, Response >,